Files
rollcall/lab/netbox/install.sh
T
Ilker ManapandClaude Opus 5.5 44ac453bde
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 43s
NetBox through Rollcall: the admin certificate signs in to NetBox
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>
2026-11-01 14:19:10 +01:00

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/