Skip to content

Configuration

Every setting NASO reads is declared in shared/config.py as a field on a single pydantic-settings Settings class. There is no second configuration mechanism — if a value is not on that class, nothing reads it.

Where values come from

Settings is configured with:

python
model_config = SettingsConfigDict(
    env_file=".env", secrets_dir="/run/secrets", extra="ignore"
)

which gives four sources, highest priority first:

  1. arguments passed to Settings(...) directly (used only by tests);
  2. process environment variables — what docker-compose.yml sets under environment:;
  3. .env in the working directory — what env_file: loads;
  4. /run/secrets/<FIELD_NAME> — one file per setting, Docker's secret convention.

Two consequences are worth internalising before you debug a configuration problem:

Files in /run/secrets are named after the field, not after an environment variable. The file that supplies the signing key is /run/secrets/JWT_PRIVATE_KEY — exactly the attribute name on Settings, uppercase, no prefix. A file called jwt_private_key or naso_jwt_private_key is ignored silently.

A value in .env beats a file in /run/secrets. This is the opposite of what most people assume, and it is the single most common configuration surprise here. cli/generate_secrets.py accounts for it by commenting out the JWT keys in the .env it renders, so the mounted secret is what actually gets used. If you edit .env by hand and uncomment them, .env wins.

extra="ignore". An unrecognised key in .env is discarded without complaint. A typo'd variable name does not fail loudly; it does nothing.

Bootstrapping

bash
make bootstrap     # python cli/generate_secrets.py

Run this once, before make up. It:

  • generates an Ed25519 key pair and random passwords for Postgres, Redis, RabbitMQ, MinIO and the initial admin;
  • writes them to .secrets-mock/, which docker-compose.yml mounts read-only at /run/secrets;
  • renders .env from .env.example, substituting the generated passwords, so the credentials the containers are provisioned with and the ones the application connects with agree.

An existing .env is left alone. Delete it first if you want it regenerated.

.secrets-mock/ is a development convenience

It is written as a 0755 directory of 0444 files — world-readable. That is not carelessness: a cap_drop: ALL container has no CAP_DAC_OVERRIDE, so it cannot ignore file permissions the way root normally can, and a 0600 file owned by your host user is simply unreadable to it. World-readable development credentials on your own machine are an acceptable trade; in production, mount real Docker secrets or a secret manager at /run/secrets instead.

Elasticsearch is deliberately not wired through this directory. It validates the mode of its own password file and accepts only 400 or 600, which contradicts the 0444 the cap_drop: ALL containers need on the same mount, so it takes ELASTIC_PASSWORD from .env instead. The reasoning is in docker-compose.yml.

Settings reference

Identity and database

SettingDefaultNotes
PROJECT_NAMENaso ForensicShown in the OpenAPI document.
DATABASE_URLpostgresql+asyncpg://naso:naso@db:5432/nasoMust use the asyncpg driver — the engine is async.
DB_POOL_SIZE20
DB_MAX_OVERFLOW10
API_TIMEOUT_SECONDS60

Authentication

SettingDefaultNotes
JWT_PRIVATE_KEY(none)Ed25519 private key, PEM. Generated by make bootstrap.
JWT_PUBLIC_KEY(none)Matching public key, PEM.
ALGORITHMEdDSA
ACCESS_TOKEN_EXPIRE_MINUTES60
JWT_ISSUERnasoAsserted on mint, verified on decode.
JWT_AUDIENCEnaso-apiSame.
JWT_LEEWAY_SECONDS30Clock-skew tolerance for exp/nbf/iat.
NASO_COOKIE_SECUREfalseSet to true in production. Read from the environment directly, not from Settings.

If you run more than one NASO deployment, give each its own JWT_ISSUER and JWT_AUDIENCE. Two deployments sharing a key pair and these values will accept each other's tokens.

Network and origins

SettingDefaultNotes
ALLOWED_CORS_ORIGINShttp://localhost:5173,http://127.0.0.1:5173,http://localhost:8000Comma-separated. Restrict to the real frontend origin in production.
REDIS_HOSTredis://naso-cache:6379/0Despite the name, a full connection URL. Backs the JWT revocation list.

Optional backing services

Elasticsearch and MinIO are optional. When their credentials are unset the application does not construct a client at all, and /system/health reports them as disabled rather than degraded.

SettingDefault
ES_HOST / ES_PORTelasticsearch / 9200
ES_USER / ES_PASSWORD(none)
MINIO_ENDPOINTminio:9000
MINIO_ACCESS_KEY / MINIO_SECRET_KEY(none)
MINIO_SECUREfalse
RABBITMQ_HOSTrabbitmq
RABBITMQ_USER / RABBITMQ_PASS(none)

AI Co-Analyst

SettingDefaultNotes
AI_ENDPOINThttp://localhost:1234/v1Any OpenAI-compatible server — LM Studio, Ollama, vLLM. From inside Docker use http://host.docker.internal:1234/v1.
AI_MODELgemma-4-e2b-it
AI_ENABLE_THINKINGfalse

See AI Co-Analyst for what the model is allowed to do.

Notifications and OSINT integrations

SettingDefault
SMTP_HOST / SMTP_PORTsmtp.naso.local / 587
SMTP_USER / SMTP_PASSWORD(none)
SMTP_FROMnaso-engine@naso.local
ENABLE_NOTIFICATIONStrue
TELEGRAM_API_ID / TELEGRAM_API_HASH(none)
TELEGRAM_SESSION_NAMEnaso_forensic_bot
SHODAN_API_KEY(none)

Scoring thresholds

SettingDefault
DEFAULT_SEVERITY_SCORE0
MAX_SEVERITY_SCORE100
CRITICAL_SCORE_THRESHOLD80

Settings not on the Settings class

A few values are read from the environment directly rather than through pydantic, usually because they are consumed before the settings singleton exists or outside the application process:

VariableRead byPurpose
NASO_COOKIE_SECUREbackend/app/api/endpoints/auth.pySecure flag on the session cookie.
NASO_ADMIN_EMAIL / NASO_ADMIN_PASSWORDbackend/init_db.pyProvisions the initial admin. init_db.py refuses to create a user without the password set — there is no default.
NASO_OTEL_ENABLEDshared/utils/*_tracing.pyTurns on OpenTelemetry export. Leave it off unless a collector is actually running; the exporter blocks at shutdown waiting for one.
NASO_DARKWEB_TOR_CONTROL_PASSWORDthe workersMust match the TOR_CONTROL_PASSWORD build arg of the Tor images.
PYTHONPATH, PYTHONDONTWRITEBYTECODEthe runtimeSet in docker-compose.yml.

Verifying a configuration change

bash
curl -s localhost:8000/system/health | jq

/system/health probes every backing service and reports each one individually — see the API reference. It is the fastest way to find out whether a credential change took effect, and which service disagrees with you.