Skip to content

Configuration ​

All configuration is managed through environment variables defined in .env. Copy .env.example to .env and adjust values for your environment.

Application Environment ​

VariableDefaultDescription
ENVIRONMENTdevelopmentSet to production to enforce JWT validation, strict CORS, the RLS startup checks, and to disable the dev email outbox

Database ​

VariableDefaultDescription
DATABASE_URLpostgresql+asyncpg://nis2:nis2secret@postgres:5432/nis2Async database connection string (used by FastAPI)
DATABASE_URL_SYNCpostgresql://nis2:nis2secret@postgres:5432/nis2Sync connection string (used by Alembic migrations)
POSTGRES_USERnis2PostgreSQL user
POSTGRES_PASSWORDnis2secretPostgreSQL password
POSTGRES_DBnis2PostgreSQL database name
MIGRATION_DATABASE_URL(falls back to DATABASE_URL)Privileged identity used ONLY for Alembic and the boot-time RLS setup. DATABASE_URL must point at a NOSUPERUSER NOBYPASSRLS role or every RLS policy is decorative — the API asserts this at startup and refuses to serve otherwise
MIGRATION_DATABASE_URL_SYNC(falls back to DATABASE_URL_SYNC)Sync variant of the above
NIS2_APP_PASSWORD—Password for the nis2_app runtime role, created on first volume init by infra/docker/initdb/01-create-app-role.sh
DB_POOL_SIZE10Connections per pool, per gunicorn worker — gunicorn preforks, so each worker builds its own
DB_MAX_OVERFLOW5Overflow per worker. Total demand is workers × (pool + overflow); keep it below the server's max_connections (100 in the stock image)

Redis ​

VariableDefaultDescription
REDIS_URLredis://redis:6379/0Redis connection for caching and sessions

Authentication (JWT) ​

VariableDefaultDescription
JWT_SECRET(change in production)Secret key for signing JWT tokens. Generate with openssl rand -hex 32
JWT_ALGORITHMHS256JWT signing algorithm
ACCESS_TOKEN_EXPIRE_MINUTES30Access token lifetime in minutes
REFRESH_TOKEN_EXPIRE_DAYS7Refresh token lifetime in days

RS256 key generation ​

bash
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

Set JWT_PRIVATE_KEY and JWT_PUBLIC_KEY to the contents of those files (newlines escaped as \n, or use multi-line block syntax in .env). The public key is then served at GET /.well-known/jwks.json for third parties to verify tokens with.

Encryption at rest ​

VariableDefaultDescription
DATA_ENCRYPTION_KEY—AES-GCM key for TOTP seeds, notification-channel credentials and leaked-secret evidence. At least 32 characters; the API refuses to boot without strong key material
DATA_ENCRYPTION_KEY_PREVIOUS(empty)The key being rotated away from. Set during a rotation only — see Secrets rotation; decryption tries the current key first and falls back to this one

Observability ​

VariableDefaultDescription
LOG_LEVELINFOVerbosity for the API and the worker. Every record carries the request id, which also returns in the X-Request-Id response header

GET /metrics exposes Prometheus series — request rate, errors and latency by route template, plus database pool utilisation. It is unauthenticated and mounted outside /api/v1, because Prometheus scrapes it over the compose network by service name; block it at the edge if the API is exposed directly. The bundled prometheus.yml already scrapes it.

GET /api/v1/health reports the running version and the commit the image was built from.

Password reset (B05) ​

The forgot/reset flow needs a public URL to put in the email link and an SMTP relay (or the dev outbox) to deliver it. In make dev and the e2e suite, leaving SMTP_HOST empty activates the in-memory dev outbox — the email is logged at INFO and captured for GET /api/v1/auth/debug/last-email (mounted only when ENVIRONMENT != "production"). Production with SMTP_HOST empty refuses to deliver: the route turns the RuntimeError into a 5xx rather than silently dropping the email.

VariableDefaultDescription
PUBLIC_URLhttp://localhost:8077Base URL the reset link points to. The user clicks ${PUBLIC_URL}/reset-password?token=…
RESET_TOKEN_TTL_MINUTES30Lifetime of the reset token. Tokens are single-use; once consumed (used_at non-null) they're rejected even within the TTL
SMTP_HOST`` (dev outbox)SMTP relay hostname. Leave empty in dev / e2e — the email is captured in-process instead
SMTP_PORT587SMTP relay port
SMTP_USER``SMTP auth username (omit if your relay doesn't require auth)
SMTP_PASSWORD``SMTP auth password
SMTP_FROMnoreply@nis2.localFrom: header on outgoing emails
SMTP_STARTTLStrueIssue STARTTLS after EHLO (the common case for ports 25 / 587)
SMTP_SSLfalseWrap the entire connection in TLS (port 465 style). Mutually exclusive with SMTP_STARTTLS

Reports ​

Generated reports (PDF / HTML / Markdown / JSON / CSV / JUnit XML) live under /tmp/nis2-reports/ on the Celery worker, shared with the API container via the reports-data Docker named volume. A daily Celery beat task (cleanup-old-reports) sweeps this directory of files older than the TTL — without it, the disk grows unbounded as users generate reports.

VariableDefaultDescription
REPORT_TTL_DAYS30Days to keep generated report files before the daily cleanup task deletes them. Long enough for a compliance team to download last week's report after a holiday; short enough that a deploy generating 100s of scans/day doesn't fill the disk in weeks. The cleanup task always runs at the schedule's wall-clock cadence regardless of this value (it just changes the cutoff age).

Certificate revocation (CertMate) ​

Whether a certificate has been revoked is asked of a CertMate instance rather than answered here. A revocation answer is only worth having if it was verified — the OCSP response signed by the issuer or a delegate it authorised, naming this certificate, and current; or a CRL issued and signed by that issuer and not past its nextUpdate — and CertMate carries that client.

Without this configured, a scan still runs and reports revocation as UNKNOWN, with the reason in the report. It never infers: a certificate that merely names an OCSP responder is not a certificate that was checked.

VariableDefaultDescription
CERTMATE_URL(unset)Base URL of the CertMate instance, e.g. https://certmate.example.com. Both this and the token are required; a URL alone is not a configuration.
CERTMATE_TOKEN(unset)An API key for that instance. A viewer key scoped to the domains this scanner is allowed to look at is enough — the scanner only reads.
CERTMATE_TIMEOUT15Seconds to wait for an answer before reporting UNKNOWN.

Install the client with the optional extra: pip install nis2scan[certmate].

Celery ​

VariableDefaultDescription
CELERY_BROKER_URLredis://redis:6379/1Celery message broker
CELERY_RESULT_BACKENDredis://redis:6379/2Celery result backend

Frontend (Next.js) ​

VariableDefaultDescription
NEXT_PUBLIC_API_URLhttp://localhost:8000Public API URL (client-side)

NEXTAUTH_URL, NEXTAUTH_SECRET and API_URL were documented here and are not read by anything: next-auth is not a dependency of packages/web. If your .env still carries them they are inert.

Production (Caddy) ​

VariableDefaultDescription
DOMAINnis2.yourdomain.comDomain for Caddy auto-HTTPS. Set this for production deployments

Scanner Defaults ​

Scanner behavior is configured per scan via the API when creating a scan or schedule. Organization settings store defaults that new scans inherit. Key defaults in the scan creation endpoint:

  • Timeout: 10 seconds per check (scan_timeout)
  • Concurrency: 20 parallel tasks (concurrency)
  • Max hosts: 0 (unlimited) -- configurable limit on targets per scan (max_hosts)
  • Features: Individual check categories (dns_checks, web_checks, port_scan, whois_checks) can be toggled per scan. Organization settings store the defaults that new scans inherit.

Organization Settings ​

Organization-level settings are managed through the dashboard under Settings:

  • Organization name and metadata
  • Default scan configuration (features, concurrency, timeout)
  • Team member management (invite, role assignment)
  • API key management
  • Notification channel preferences

Row-Level Security ​

VariableDefaultDescription
RLS_SUPERUSER_OK—Set to 1 to suppress the startup error when the database role is SUPERUSER or BYPASSRLS. Not recommended for production — provision a non-superuser app role instead