Feature Specification: Component netbox
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/components/hosting-provider/netbox/default.nix, core/components/hosting-provider/netbox/oidc.nix, core/components/hosting-provider/netbox.nix, core/components/hosting-provider/netbox-provision-token.py, core/components/hosting-provider/netbox-bootstrap.nix, core/components/hosting-provider/netbox-bootstrap-driver.py, core/components/hosting-provider/netbox-bootstrap-rest.py, core/components/hosting-provider/netbox-test.nix, core/components/hosting-provider/netbox/running-test.nix, core/resources/netbox/self-hosted.nix, core/setups/wiring/netbox-routing.nix, core/checks/default.nix, core/checks/ci.nix), pull requests #1400, #1482, #1490, #1542, #1597, #1822, #1852 and #1996.
User Scenarios & Testing (mandatory)
User Story 1 - Run an IPAM instance for the deployment (Priority: P1)
A hosting provider has no NetBox instance of its own. The netbox component
runs NetBox on a node of the hosting-provider group. The deploy programs
then allocate the addresses of the nodes from this instance, through the
netbox resource (see netbox). The
component declares provides = [ "netbox" ].
Why this priority: without an IPAM instance, the nodes keep the
placeholder address 0.0.0.0, and the deployment cannot publish a zone.
Independent Test: test-netbox-service. It imports netbox.nix on one
node, waits for netbox.service, nginx.service and
netbox-provision-token.service, and runs the four tf-netbox-* effects
against the instance.
Acceptance Scenarios:
- Given a node that imports
netbox.nix, When it boots, Then gunicorn listens on port8001and nginx serves the vhostnetbox.<apex>on port443. - Given a running instance, When the
tf-netbox-store-ipsandtf-netbox-get-ipeffects run, Then the instance gives the first free address of the range. - Given a deployment with an external NetBox (
serverUrlis set), When the setups evaluate, Then the roster does not contain the node that providesnetbox, and theoverviewpage shows no tile for it.
User Story 2 - Give the operator deploy an API token (Priority: P1)
The operator deploy must authenticate to NetBox to allocate addresses. The
netbox-provision-token unit mints a write-enabled API token for the service
user fediversity-operator through netbox-manage shell. On a node whose
vars route to OpenBao, it writes the token to KV at
<prefix>/secret/netbox/apiToken, so the deploy reads it from there.
NetBox keeps only the key and a digest of a v2 token, so KV holds the full
credential, nbt_<key>.<plaintext>. The unit gives that credential back to
the script on each run, and the script keeps it while it authenticates.
Why this priority: the self-hosted provider of the netbox contract
gives apiTokenFile = "". The token in KV is the only credential that the
incus graph has for the instance.
Independent Test: test-netbox-service waits for
netbox-provision-token.service with the on-machine vars backend.
test-netbox-consumers runs the unit with the OpenBao vars backend. It reads
the token at <prefix>/secret/netbox/apiToken, checks that
/api/authentication-check/ accepts the token with Authorization: Bearer
as fediversity-operator and refuses the key with a wrong plaintext. It checks
that a second run publishes the same token, and that a run after the token is
removed from NetBox publishes a new token that authenticates.
nix-unit-components-netbox-consumers checks that the unit writes the token
to KV only with the OpenBao vars backend, to an instance that the node can
write.
Acceptance Scenarios:
- Given a running instance with the OpenBao vars backend, When
netbox-provision-tokenruns two times, Then KV holds the same token after both runs. - Given
services.vars-openbao.varsBackendis empty, When the unit runs, Then it mints the token and does not callbao. - Given
netbox.servicefails its first start, When NetBox recovers, Thennetbox-provision-tokenretries every 30 seconds and mints the token.
User Story 3 - Log in, send mail and report telemetry (Priority: P2)
An administrator logs in to the NetBox UI with single sign-on. NetBox sends
notification mail through the deployment relay, and reports traces to the
OpenTelemetry collector. The component requests these through the oidc,
smtp and otel resources.
Why this priority: the instance works without them, but an administrator then needs a local password, and failures are not visible.
Independent Test: nix-unit-components-netbox-consumers checks the
configuration that the node renders from the oidc, smtp, otel and
postgresql results. test-netbox-consumers runs the consumer modules on one
node with stand-in results. NetBox reads the API token from PostgreSQL with
the password of the result. netbox and netbox-rq load the OIDC client
secret and the SMTP password from OpenBao KV. A POST to /oauth/begin/oidc/
with the CSRF token of the login page, as the SSO button sends it, redirects
to the issuer with client_id=netbox. A mail from netbox-manage sendtestemail reaches a stand-in relay with the sender of the result. The
collector logs traces with service.name netbox.
Acceptance Scenarios:
- Given an
oidcprovider in the deployment, Whennetbox.servicestarts, Then it loads the credentialoidc-client-secretand setsSOCIAL_AUTH_OIDC_SECRETfrom it. - Given the credential arrives empty, When a unit loads
configuration.py, Then the unit fails, andRestart=on-failureretries it. - Given an
https://issuer and a provisionedfediversity.openbao-pki.caCertFile, Whennetbox.servicestarts, Then it starts afteropenbao-pki-trust.serviceand hasREQUESTS_CA_BUNDLEset.
Edge Cases
netbox-manageruns outside a unit.CREDENTIALS_DIRECTORYis not set, soconfiguration.pyskips the OIDC secret and the SMTP password and does not fail.- The database password rotates. The password is read once at process start, so the
postgresqlrequest listsnetbox.serviceandnetbox-rq.serviceinreloadUnits. - The nixpkgs module declares a
netbox-housekeepingtimer without a service. The component disables the timer. - The
otelwrapper needs the patched package of the upstream module. The component reads its store path out of thenetbox migrateline ofnetbox.service'spreStart. A change of that line in nixpkgs breaks the evaluation.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
components.netboxwithname = "NetBox",package = pkgs.netbox, the upstream icon,uiSubdomain = "netbox"andcertAudience = "browser". - FR-002: The settings of the component MUST come from
sharedOptions.nixwithdefaults.subdomain = "netbox", sodomainis derived fromsubdomainand the deployment domain. - FR-003: When
enableis true, theimplementationMUST requesttls.ssl.domains = [ domain ](seessl). - FR-004: The
implementationMUST request a PostgreSQL databasenetboxwith usernetbox, a password file owned bynetboxwith mode0400, andreloadUnitsnetbox.serviceandnetbox-rq.service(seepostgresql). - FR-005: The
netbox-dbrequest MUST contain only data. Thecodemodule MUST render its client (database.nix) fromcontracts.postgresql.results.netbox.default, MUST render nothing when the result is absent, and when the result is present MUST forceservices.netbox.settings.DATABASES.defaultto the result coordinates, MUST read the password fromresult.passwordFileinextraConfig, and MUST setservices.netbox.postgresql.createLocally = false. - FR-006: The
implementationMUST request anoidcclientnetboxwith the redirect URIhttps://<domain>/oauth/complete/oidc/(seeoidc). - FR-007: The OIDC consumer MUST use the
python-social-authbackendOpenIdConnectAuthwithSOCIAL_AUTH_OIDC_TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_post", and MUST load the client secret as the credentialoidc-client-secretintonetboxandnetbox-rqfromfediversity.oidc.clientSecretKv. - FR-008: The OIDC consumer MUST set
StartLimitIntervalSec = 0on both units, so restarts during a deploy do not stop the retries. - FR-009: The
implementationMUST request ansmtpsendernoreply@<domain>, MUST setservices.netbox.settings.EMAILfrom the result withUSE_TLS = true, and MUST load the password as the credentialsmtp-passwordintonetboxandnetbox-rq(seesmtp). - FR-010: The
implementationMUST requestotel, and MUST runnetboxandnetbox-rqthroughopentelemetry-instrumentwithOTEL_SERVICE_NAMEset to the unit name andDJANGO_SETTINGS_MODULE = "netbox.settings"(seeotel). - FR-011: The
implementationMUST requestspiffe.enable = true(seespiffe) and MUST importnetbox.nixthrough thenixos-moduleresource (seenixos-module). - FR-012:
netbox.nixMUST generatesecretKeyFileandapiTokenPeppersFilethrough thegenerateFilescontract, and MUST map the pepper file to the pepper ID"1"(seegenerateFiles). - FR-013:
netbox.nixMUST runpkgs.netbox_4_7on0.0.0.0:8001, the port incore/resources/netbox/gunicorn-port.nixthat theself-hostedprovider also reads, and MUST ordernetbox.serviceaftergenerate-vars.serviceandopenbao-agent-default.service. - FR-014:
netbox.nixMUST serve the vhost that thedomainsetting of the component names, whichnetbox/default.nixsets as the optionfediversity.netbox.domain, with a proxy to127.0.0.1:8001and an alias for/static/, and MUST addnginxto the groupnetboxwhen nginx is enabled. - FR-015:
netbox-provision-tokenMUST be aoneshotwithRemainAfterExit,Restart = "on-failure",RestartSec = "30s"andStartLimitIntervalSec = 0, and MUST useWants=onnetbox.service, notRequires=. - FR-016:
netbox-provision-tokenMUST reuse the service user, MUST print the credential that it reads on stdin when that credential is an active v2 token of the user thatToken.validateaccepts, MUST otherwise delete the tokens of the user, mint one v2 token and printnbt_<key>.<plaintext>, and MUST read the token from OpenBao KV, and write it there when it changes, base64-encoded undercontent, only whenservices.vars-openbao.varsBackendis not empty. - FR-017: The component MUST declare
provides = [ "netbox" ]. With an external NetBox, the roster MUST drop the node that providesnetbox. - FR-018: The
implementationMUST requestgaragethroughmkBackupsGarageRequestwith the bucketnetbox-backups, and thecodemodule MUST add astreamingBackupof thenetbox-dbdatabase to that bucket with the restic provider, as the usernetbox.
Key Entities
netboxcomponent: the declaration innetbox/default.nix, with its settings and its resource requests.netbox.nix: the NixOS module of the node: NetBox, nginx, the generated secrets and the token unit.- Service user
fediversity-operator: the NetBox user that owns the API token of the operator deploy. - API token: the write-enabled v2 token that
netbox-provision-token.pymints, and that the unit publishes at<prefix>/secret/netbox/apiToken. - Test bootstrap:
netbox-bootstrap.nixwithnetbox-bootstrap-driver.pyandnetbox-bootstrap-rest.py. These are test-driver helpers fortest-netbox-serviceandapps-tf-incus, not part of the component. They make a superusernetboxand a v1 token, and look up addresses through the REST API.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
test-netbox-servicepasses. The instance answers onhttp://127.0.0.1:8001/login/,netbox-provision-token.serviceis active, andtf-netbox-get-ipgives192.168.10.236/24. - SC-002:
apps-tf-incuspasses with thenetboxnode as the IPAM of the incus graph. - SC-003:
nix-unit-setups-netbox-routing,nix-unit-setups-netbox-pinningandnix-unit-setups-overview-uispass. - SC-004:
nix-unit-components-netbox-consumerspasses. - SC-005:
test-netbox-consumerspasses.
Assumptions
- The
self-hostedprovider of thenetboxcontract reaches the instance athttp://<host>:8001, where<host>is the FQDN of the node that providesnetbox. This is gunicorn directly. So no in-cluster TLS trust is necessary during the bootstrap. - The incus graph reads the token from KV only from the second apply on, because the vault provider dials an address that the previous apply published.
- The catalog metadata comes from
pkgs.netbox, and the node runspkgs.netbox_4_7. - The
otelwrapper copies theExecStartlines of the nixpkgs module, because that module has no hook for a wrapper.
Known Deviations
test-netbox-consumersstops the OIDC check at the redirect to the provider. No check completes a login against a real provider.- In
test-netbox-consumers,netbox-managesends the test mail with a copy of the credentials ofnetbox.service. No check sends a mail from thenetboxornetbox-rqprocess. test-netbox-consumersconnects to PostgreSQL withsslMode = "disable". No check runs the TLS connection or a password rotation throughreloadUnits.- The service user
fediversity-operatorhas no object permission and is not a superuser. No check uses its token for an IPAM request.