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:

  1. Given a node that imports netbox.nix, When it boots, Then gunicorn listens on port 8001 and nginx serves the vhost netbox.<apex> on port 443.
  2. Given a running instance, When the tf-netbox-store-ips and tf-netbox-get-ip effects run, Then the instance gives the first free address of the range.
  3. Given a deployment with an external NetBox (serverUrl is set), When the setups evaluate, Then the roster does not contain the node that provides netbox, and the overview page 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:

  1. Given a running instance with the OpenBao vars backend, When netbox-provision-token runs two times, Then KV holds the same token after both runs.
  2. Given services.vars-openbao.varsBackend is empty, When the unit runs, Then it mints the token and does not call bao.
  3. Given netbox.service fails its first start, When NetBox recovers, Then netbox-provision-token retries 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:

  1. Given an oidc provider in the deployment, When netbox.service starts, Then it loads the credential oidc-client-secret and sets SOCIAL_AUTH_OIDC_SECRET from it.
  2. Given the credential arrives empty, When a unit loads configuration.py, Then the unit fails, and Restart=on-failure retries it.
  3. Given an https:// issuer and a provisioned fediversity.openbao-pki.caCertFile, When netbox.service starts, Then it starts after openbao-pki-trust.service and has REQUESTS_CA_BUNDLE set.

Edge Cases

  • netbox-manage runs outside a unit. CREDENTIALS_DIRECTORY is not set, so configuration.py skips 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 postgresql request lists netbox.service and netbox-rq.service in reloadUnits.
  • The nixpkgs module declares a netbox-housekeeping timer without a service. The component disables the timer.
  • The otel wrapper needs the patched package of the upstream module. The component reads its store path out of the netbox migrate line of netbox.service's preStart. A change of that line in nixpkgs breaks the evaluation.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare components.netbox with name = "NetBox", package = pkgs.netbox, the upstream icon, uiSubdomain = "netbox" and certAudience = "browser".
  • FR-002: The settings of the component MUST come from sharedOptions.nix with defaults.subdomain = "netbox", so domain is derived from subdomain and the deployment domain.
  • FR-003: When enable is true, the implementation MUST request tls.ssl.domains = [ domain ] (see ssl).
  • FR-004: The implementation MUST request a PostgreSQL database netbox with user netbox, a password file owned by netbox with mode 0400, and reloadUnits netbox.service and netbox-rq.service (see postgresql).
  • FR-005: The netbox-db request MUST contain only data. The code module MUST render its client (database.nix) from contracts.postgresql.results.netbox.default, MUST render nothing when the result is absent, and when the result is present MUST force services.netbox.settings.DATABASES.default to the result coordinates, MUST read the password from result.passwordFile in extraConfig, and MUST set services.netbox.postgresql.createLocally = false.
  • FR-006: The implementation MUST request an oidc client netbox with the redirect URI https://<domain>/oauth/complete/oidc/ (see oidc).
  • FR-007: The OIDC consumer MUST use the python-social-auth backend OpenIdConnectAuth with SOCIAL_AUTH_OIDC_TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_post", and MUST load the client secret as the credential oidc-client-secret into netbox and netbox-rq from fediversity.oidc.clientSecretKv.
  • FR-008: The OIDC consumer MUST set StartLimitIntervalSec = 0 on both units, so restarts during a deploy do not stop the retries.
  • FR-009: The implementation MUST request an smtp sender noreply@<domain>, MUST set services.netbox.settings.EMAIL from the result with USE_TLS = true, and MUST load the password as the credential smtp-password into netbox and netbox-rq (see smtp).
  • FR-010: The implementation MUST request otel, and MUST run netbox and netbox-rq through opentelemetry-instrument with OTEL_SERVICE_NAME set to the unit name and DJANGO_SETTINGS_MODULE = "netbox.settings" (see otel).
  • FR-011: The implementation MUST request spiffe.enable = true (see spiffe) and MUST import netbox.nix through the nixos-module resource (see nixos-module).
  • FR-012: netbox.nix MUST generate secretKeyFile and apiTokenPeppersFile through the generateFiles contract, and MUST map the pepper file to the pepper ID "1" (see generateFiles).
  • FR-013: netbox.nix MUST run pkgs.netbox_4_7 on 0.0.0.0:8001, the port in core/resources/netbox/gunicorn-port.nix that the self-hosted provider also reads, and MUST order netbox.service after generate-vars.service and openbao-agent-default.service.
  • FR-014: netbox.nix MUST serve the vhost that the domain setting of the component names, which netbox/default.nix sets as the option fediversity.netbox.domain, with a proxy to 127.0.0.1:8001 and an alias for /static/, and MUST add nginx to the group netbox when nginx is enabled.
  • FR-015: netbox-provision-token MUST be a oneshot with RemainAfterExit, Restart = "on-failure", RestartSec = "30s" and StartLimitIntervalSec = 0, and MUST use Wants= on netbox.service, not Requires=.
  • FR-016: netbox-provision-token MUST reuse the service user, MUST print the credential that it reads on stdin when that credential is an active v2 token of the user that Token.validate accepts, MUST otherwise delete the tokens of the user, mint one v2 token and print nbt_<key>.<plaintext>, and MUST read the token from OpenBao KV, and write it there when it changes, base64-encoded under content, only when services.vars-openbao.varsBackend is not empty.
  • FR-017: The component MUST declare provides = [ "netbox" ]. With an external NetBox, the roster MUST drop the node that provides netbox.
  • FR-018: The implementation MUST request garage through mkBackupsGarageRequest with the bucket netbox-backups, and the code module MUST add a streamingBackup of the netbox-db database to that bucket with the restic provider, as the user netbox.

Key Entities

  • netbox component: the declaration in netbox/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.py mints, and that the unit publishes at <prefix>/secret/netbox/apiToken.
  • Test bootstrap: netbox-bootstrap.nix with netbox-bootstrap-driver.py and netbox-bootstrap-rest.py. These are test-driver helpers for test-netbox-service and apps-tf-incus, not part of the component. They make a superuser netbox and a v1 token, and look up addresses through the REST API.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: test-netbox-service passes. The instance answers on http://127.0.0.1:8001/login/, netbox-provision-token.service is active, and tf-netbox-get-ip gives 192.168.10.236/24.
  • SC-002: apps-tf-incus passes with the netbox node as the IPAM of the incus graph.
  • SC-003: nix-unit-setups-netbox-routing, nix-unit-setups-netbox-pinning and nix-unit-setups-overview-uis pass.
  • SC-004: nix-unit-components-netbox-consumers passes.
  • SC-005: test-netbox-consumers passes.

Assumptions

  • The self-hosted provider of the netbox contract reaches the instance at http://<host>:8001, where <host> is the FQDN of the node that provides netbox. 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 runs pkgs.netbox_4_7.
  • The otel wrapper copies the ExecStart lines of the nixpkgs module, because that module has no hook for a wrapper.

Known Deviations

  • test-netbox-consumers stops the OIDC check at the redirect to the provider. No check completes a login against a real provider.
  • In test-netbox-consumers, netbox-manage sends the test mail with a copy of the credentials of netbox.service. No check sends a mail from the netbox or netbox-rq process.
  • test-netbox-consumers connects to PostgreSQL with sslMode = "disable". No check runs the TLS connection or a password rotation through reloadUnits.
  • The service user fediversity-operator has no object permission and is not a superuser. No check uses its token for an IPAM request.