With ipam.ui_login_secret_file, the proxy tells NetBox who the admin is (X-Rollcall-User: the certificate's admin name) together with a shared secret (X-Rollcall-Proxy-Secret). NetBox takes the user from that header (REMOTE_AUTH) and creates it on first use; the web server in front of NetBox drops the header from any request that lacks the secret or does not come from the region, so reaching NetBox directly claims no one. Both headers coming from a browser are always dropped by Rollcall; an admin signed in with a token instead of a certificate logs in to NetBox by hand. A secret under 16 characters or an unreadable file leaves the proxy off, with a log line. The lab NetBox install sets it up: REMOTE_AUTH settings, a guarded Caddyfile (readable by caddy only: the secret is in it), the secret in secrets.env, and a rollcall-admins group that may do everything. Rollcall's NetBox user may now view cables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
9.5 KiB
Bash
184 lines
9.5 KiB
Bash
#!/bin/bash
|
|
# Lab NetBox for Rollcall's NetBox drivers (IPAM, device sync): a Debian 13
|
|
# container on the lab segment, so tests never write to a real NetBox.
|
|
# Same install as netops (altyapi hosts/netops/kurulum.sh) without PowerDNS,
|
|
# ACME and backups; plain HTTP on :80 (the segment is isolated).
|
|
# NetBox lives under /netbox/ (BASE_PATH) so the region can serve its UI
|
|
# through Rollcall (cmd/rollcall/nbproxy.go): Rollcall's ipam url is then
|
|
# http://$MYIP/netbox/, and ROLLCALL_ORIGINS are where browsers post from.
|
|
# The admin certificate signs in to NetBox through that proxy: NetBox takes
|
|
# the user from X-Rollcall-User (REMOTE_AUTH), and Caddy drops that header
|
|
# unless the request comes from ROLLCALL_IP with ROLLCALL_UI_SECRET (copy it
|
|
# to the region's ipam.ui_login_secret_file). Such users land in the group
|
|
# rollcall-admins, which may do everything (a lab).
|
|
#
|
|
# Run as root inside the container; rerunnable. Secrets are generated here:
|
|
# /etc/lab-netbox/secrets.env (admin password, Rollcall's API token)
|
|
#
|
|
# On pve1 (example):
|
|
# pct create 225 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst --hostname lab-netbox \
|
|
# --cores 2 --memory 2048 --swap 512 --rootfs zfspool:8 --unprivileged 1 \
|
|
# --net0 name=eth0,bridge=pxenet,ip=10.213.77.6/24,gw=10.213.77.1 --nameserver 10.213.77.1
|
|
# pct start 225 && pct push 225 install.sh /root/install.sh && pct exec 225 -- bash /root/install.sh
|
|
set -euo pipefail
|
|
NETBOX_VERSION=${NETBOX_VERSION:-4.7.4}
|
|
MYIP=${MYIP:-10.213.77.6}
|
|
SITE=${SITE:-lab}
|
|
ROLLCALL_ORIGINS=${ROLLCALL_ORIGINS:-"https://10.0.0.234 https://10.213.77.1"}
|
|
ROLLCALL_IP=${ROLLCALL_IP:-10.213.77.1}
|
|
PREFIXES=${PREFIXES:-"10.213.77.0/24 10.213.78.0/24 10.213.79.0/24 fd00:213:77::/64 fd00:213:78::/64"}
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
apt-get update -q
|
|
apt-get install -yq postgresql valkey-server caddy \
|
|
python3 python3-venv python3-dev build-essential libxml2-dev libxslt1-dev libffi-dev \
|
|
libpq-dev libssl-dev zlib1g-dev git curl jq
|
|
|
|
install -d -m 700 /etc/lab-netbox
|
|
S=/etc/lab-netbox/secrets.env
|
|
if [ ! -f $S ]; then
|
|
r() { python3 -c "import secrets,sys;print(secrets.token_urlsafe(int(sys.argv[1])))" "$1"; }
|
|
cat > $S <<EOF
|
|
NETBOX_DB_PASS=$(r 24)
|
|
NETBOX_SECRET_KEY=$(r 50)
|
|
NETBOX_PEPPER=$(r 50)
|
|
NETBOX_ADMIN_PASS=$(r 18)
|
|
ROLLCALL_PASS=$(r 18)
|
|
EOF
|
|
chmod 600 $S
|
|
fi
|
|
grep -q '^ROLLCALL_UI_SECRET=' $S || echo "ROLLCALL_UI_SECRET=$(python3 -c 'import secrets;print(secrets.token_urlsafe(32))')" >> $S
|
|
. $S
|
|
|
|
# ------------------------------------------------------------------ PostgreSQL, Valkey
|
|
sudo() { runuser -u "$2" -- "${@:3}"; } # "sudo -u postgres ..." without sudo
|
|
if [ -z "$(sudo -u postgres psql -tAqc "SELECT 1 FROM pg_roles WHERE rolname='netbox'")" ]; then
|
|
sudo -u postgres psql -qc "CREATE ROLE netbox LOGIN PASSWORD '$NETBOX_DB_PASS'"
|
|
# UTF-8 whatever the container's locale (C in the Proxmox template)
|
|
sudo -u postgres psql -qc "CREATE DATABASE netbox OWNER netbox ENCODING 'UTF8' LC_COLLATE 'C.UTF-8' LC_CTYPE 'C.UTF-8' TEMPLATE template0"
|
|
fi
|
|
sed -i 's/^# *databases .*/databases 16/' /etc/valkey/valkey.conf || true
|
|
systemctl enable --now valkey-server
|
|
|
|
# ------------------------------------------------------------------ NetBox
|
|
id netbox >/dev/null 2>&1 || adduser --system --group --home /opt/netbox netbox
|
|
D=/opt/netbox-$NETBOX_VERSION
|
|
if [ ! -d $D ]; then
|
|
curl -fsSL https://github.com/netbox-community/netbox/archive/refs/tags/v$NETBOX_VERSION.tar.gz | tar -xz -C /opt
|
|
fi
|
|
[ -L /opt/netbox ] || { [ -d /opt/netbox ] && rmdir /opt/netbox 2>/dev/null || true; }
|
|
ln -sfn $D /opt/netbox
|
|
cat > /opt/netbox/netbox/netbox/configuration.py <<EOF
|
|
# pxestuff lab/netbox/install.sh generates this file
|
|
ALLOWED_HOSTS = ['$MYIP', 'localhost', '127.0.0.1']
|
|
DATABASES = {'default': {'NAME': 'netbox', 'USER': 'netbox', 'PASSWORD': '$NETBOX_DB_PASS',
|
|
'HOST': '127.0.0.1', 'PORT': '', 'CONN_MAX_AGE': 300}}
|
|
REDIS = {
|
|
'tasks': {'HOST': '127.0.0.1', 'PORT': 6379, 'USERNAME': '', 'PASSWORD': '', 'DATABASE': 0, 'SSL': False},
|
|
'caching': {'HOST': '127.0.0.1', 'PORT': 6379, 'USERNAME': '', 'PASSWORD': '', 'DATABASE': 1, 'SSL': False},
|
|
}
|
|
SECRET_KEY = '$NETBOX_SECRET_KEY'
|
|
API_TOKEN_PEPPERS = {1: '$NETBOX_PEPPER'}
|
|
TIME_ZONE = 'Europe/Stockholm'
|
|
BASE_PATH = 'netbox/'
|
|
REMOTE_AUTH_ENABLED = True
|
|
REMOTE_AUTH_BACKEND = 'netbox.authentication.RemoteUserBackend'
|
|
REMOTE_AUTH_HEADER = 'HTTP_X_ROLLCALL_USER'
|
|
REMOTE_AUTH_AUTO_CREATE_USER = True
|
|
REMOTE_AUTH_DEFAULT_GROUPS = ['rollcall-admins']
|
|
CSRF_TRUSTED_ORIGINS = ['http://$MYIP'$(for o in $ROLLCALL_ORIGINS; do printf ", '%s'" "$o"; done)]
|
|
EOF
|
|
chown -R netbox:netbox $D/netbox/media $D/netbox/reports $D/netbox/scripts 2>/dev/null || true
|
|
cd /opt/netbox && PYTHON=/usr/bin/python3 ./upgrade.sh >/var/log/netbox-upgrade.log 2>&1 || { tail -40 /var/log/netbox-upgrade.log; exit 1; }
|
|
chown -R netbox:netbox /opt/netbox/netbox/media
|
|
cp /opt/netbox/contrib/gunicorn.py /opt/netbox/gunicorn.py
|
|
sed -i "s/^bind = .*/bind = '127.0.0.1:8001'/" /opt/netbox/gunicorn.py
|
|
cp /opt/netbox/contrib/netbox.service /opt/netbox/contrib/netbox-rq.service /etc/systemd/system/
|
|
systemctl daemon-reload
|
|
systemctl enable --now netbox netbox-rq
|
|
systemctl restart netbox netbox-rq
|
|
|
|
cat > /etc/caddy/Caddyfile <<EOF
|
|
# pxestuff lab/netbox/install.sh
|
|
http://$MYIP, http://localhost {
|
|
# who signs in comes only from Rollcall's proxy, with the shared secret
|
|
@notrollcall not {
|
|
remote_ip $ROLLCALL_IP
|
|
header X-Rollcall-Proxy-Secret "$ROLLCALL_UI_SECRET"
|
|
}
|
|
request_header @notrollcall -X-Rollcall-User
|
|
request_header -X-Rollcall-Proxy-Secret
|
|
redir / /netbox/
|
|
handle_path /netbox/static/* {
|
|
root * /opt/netbox/netbox/static
|
|
file_server
|
|
}
|
|
reverse_proxy 127.0.0.1:8001
|
|
}
|
|
EOF
|
|
chmod 640 /etc/caddy/Caddyfile && chgrp caddy /etc/caddy/Caddyfile # the secret is in it
|
|
systemctl enable caddy && systemctl restart caddy
|
|
|
|
# ------------------------------------------------------------------ what Rollcall needs
|
|
# admin and rollcall users, the ownership tag and field, Rollcall's permissions
|
|
# (as altyapi hosts/netops/seed/rollcall-izinler.py on netops), the lab site
|
|
# and its prefixes delegated to Rollcall (tagged).
|
|
NB_ADMIN_PASS=$NETBOX_ADMIN_PASS NB_ROLLCALL_PASS=$ROLLCALL_PASS NB_SITE=$SITE NB_PREFIXES=$PREFIXES \
|
|
/opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py shell -c '
|
|
import os
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from users.models import User, ObjectPermission
|
|
from extras.models import Tag, CustomField
|
|
from dcim.models import Site
|
|
from ipam.models import Prefix
|
|
|
|
def user(name, pw, su):
|
|
u, _ = User.objects.get_or_create(username=name)
|
|
u.set_password(pw); u.is_superuser = su; u.is_active = True; u.save()
|
|
return u
|
|
|
|
user("admin", os.environ["NB_ADMIN_PASS"], True)
|
|
rc = user("rollcall", os.environ["NB_ROLLCALL_PASS"], False)
|
|
tag, _ = Tag.objects.get_or_create(slug="rollcall", defaults={"name": "rollcall", "color": "2196f3"})
|
|
cf, _ = CustomField.objects.get_or_create(name="rollcall_machine", defaults={"type": "text", "label": "Rollcall machine"})
|
|
for m in ("ipam.ipaddress", "dcim.device"):
|
|
cf.object_types.add(ContentType.objects.get_by_natural_key(*m.split(".")))
|
|
perms = {
|
|
"rollcall-ipam-write": (["ipam.ipaddress"], ["view", "add", "change", "delete"]),
|
|
"rollcall-read": (["ipam.prefix", "ipam.vlan", "ipam.iprange", "extras.tag", "dcim.site"], ["view"]),
|
|
"rollcall-dcim-write": (["dcim.device", "dcim.interface", "dcim.macaddress"], ["view", "add", "change", "delete"]),
|
|
"rollcall-dcim-types": (["dcim.manufacturer", "dcim.devicetype", "dcim.devicerole"], ["view", "add"]),
|
|
"rollcall-dcim-read": (["dcim.location", "dcim.rack", "dcim.cable"], ["view"]),
|
|
}
|
|
for name, (types, actions) in perms.items():
|
|
p, _ = ObjectPermission.objects.get_or_create(name=name, defaults={"actions": actions})
|
|
p.actions = actions; p.enabled = True; p.save()
|
|
p.object_types.set([ContentType.objects.get_by_natural_key(*t.split(".")) for t in types])
|
|
p.users.add(rc)
|
|
from users.models import Group
|
|
from django.apps import apps
|
|
admins, _ = Group.objects.get_or_create(name="rollcall-admins") # who the admin certificate signs in
|
|
p, _ = ObjectPermission.objects.get_or_create(name="rollcall-admins-all", defaults={"actions": ["view", "add", "change", "delete"]})
|
|
p.actions = ["view", "add", "change", "delete"]; p.enabled = True; p.save()
|
|
p.object_types.set([ContentType.objects.get_for_model(m) for a in ("dcim", "ipam", "circuits", "tenancy", "virtualization", "extras", "wireless", "vpn") for m in apps.get_app_config(a).get_models()])
|
|
p.groups.add(admins)
|
|
site, _ = Site.objects.get_or_create(slug=os.environ["NB_SITE"], defaults={"name": os.environ["NB_SITE"].capitalize()})
|
|
for cidr in os.environ["NB_PREFIXES"].split():
|
|
p, _ = Prefix.objects.get_or_create(prefix=cidr, defaults={"description": "Rollcall lab", "scope": site})
|
|
p.tags.add(tag)
|
|
print("rollcall objects ready")
|
|
'
|
|
|
|
# Rollcall's API token (v2), made once through the API with its password
|
|
if ! grep -q '^NETBOX_TOKEN_ROLLCALL=' $S; then
|
|
curl -fsS http://127.0.0.1:8001/netbox/api/users/tokens/provision/ -H "Host: $MYIP" \
|
|
-H 'Content-Type: application/json' -H 'Accept: application/json' \
|
|
-d "{\"username\":\"rollcall\",\"password\":\"$ROLLCALL_PASS\",\"description\":\"rollcall region\",\"write_enabled\":true}" |
|
|
python3 -c 'import json,sys;d=json.load(sys.stdin);print("NETBOX_TOKEN_ROLLCALL=nbt_%s.%s"%(d["key"],d["token"]))' >> $S
|
|
fi
|
|
|
|
echo "--- ready"
|
|
systemctl is-active postgresql valkey-server netbox netbox-rq caddy | paste -sd' '
|
|
. $S
|
|
curl -fsS -o /dev/null -w "rollcall token: %{http_code}\n" -H "Authorization: Bearer $NETBOX_TOKEN_ROLLCALL" http://$MYIP/netbox/api/ipam/prefixes/
|