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:
model_config = SettingsConfigDict(
env_file=".env", secrets_dir="/run/secrets", extra="ignore"
)which gives four sources, highest priority first:
- arguments passed to
Settings(...)directly (used only by tests); - process environment variables — what
docker-compose.ymlsets underenvironment:; .envin the working directory — whatenv_file:loads;/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
make bootstrap # python cli/generate_secrets.pyRun 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/, whichdocker-compose.ymlmounts read-only at/run/secrets; - renders
.envfrom.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
| Setting | Default | Notes |
|---|---|---|
PROJECT_NAME | Naso Forensic | Shown in the OpenAPI document. |
DATABASE_URL | postgresql+asyncpg://naso:naso@db:5432/naso | Must use the asyncpg driver — the engine is async. |
DB_POOL_SIZE | 20 | |
DB_MAX_OVERFLOW | 10 | |
API_TIMEOUT_SECONDS | 60 |
Authentication
| Setting | Default | Notes |
|---|---|---|
JWT_PRIVATE_KEY | (none) | Ed25519 private key, PEM. Generated by make bootstrap. |
JWT_PUBLIC_KEY | (none) | Matching public key, PEM. |
ALGORITHM | EdDSA | |
ACCESS_TOKEN_EXPIRE_MINUTES | 60 | |
JWT_ISSUER | naso | Asserted on mint, verified on decode. |
JWT_AUDIENCE | naso-api | Same. |
JWT_LEEWAY_SECONDS | 30 | Clock-skew tolerance for exp/nbf/iat. |
NASO_COOKIE_SECURE | false | Set 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
| Setting | Default | Notes |
|---|---|---|
ALLOWED_CORS_ORIGINS | http://localhost:5173,http://127.0.0.1:5173,http://localhost:8000 | Comma-separated. Restrict to the real frontend origin in production. |
REDIS_HOST | redis://naso-cache:6379/0 | Despite 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.
| Setting | Default |
|---|---|
ES_HOST / ES_PORT | elasticsearch / 9200 |
ES_USER / ES_PASSWORD | (none) |
MINIO_ENDPOINT | minio:9000 |
MINIO_ACCESS_KEY / MINIO_SECRET_KEY | (none) |
MINIO_SECURE | false |
RABBITMQ_HOST | rabbitmq |
RABBITMQ_USER / RABBITMQ_PASS | (none) |
AI Co-Analyst
| Setting | Default | Notes |
|---|---|---|
AI_ENDPOINT | http://localhost:1234/v1 | Any OpenAI-compatible server — LM Studio, Ollama, vLLM. From inside Docker use http://host.docker.internal:1234/v1. |
AI_MODEL | gemma-4-e2b-it | |
AI_ENABLE_THINKING | false |
See AI Co-Analyst for what the model is allowed to do.
Notifications and OSINT integrations
| Setting | Default |
|---|---|
SMTP_HOST / SMTP_PORT | smtp.naso.local / 587 |
SMTP_USER / SMTP_PASSWORD | (none) |
SMTP_FROM | naso-engine@naso.local |
ENABLE_NOTIFICATIONS | true |
TELEGRAM_API_ID / TELEGRAM_API_HASH | (none) |
TELEGRAM_SESSION_NAME | naso_forensic_bot |
SHODAN_API_KEY | (none) |
Scoring thresholds
| Setting | Default |
|---|---|
DEFAULT_SEVERITY_SCORE | 0 |
MAX_SEVERITY_SCORE | 100 |
CRITICAL_SCORE_THRESHOLD | 80 |
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:
| Variable | Read by | Purpose |
|---|---|---|
NASO_COOKIE_SECURE | backend/app/api/endpoints/auth.py | Secure flag on the session cookie. |
NASO_ADMIN_EMAIL / NASO_ADMIN_PASSWORD | backend/init_db.py | Provisions the initial admin. init_db.py refuses to create a user without the password set — there is no default. |
NASO_OTEL_ENABLED | shared/utils/*_tracing.py | Turns on OpenTelemetry export. Leave it off unless a collector is actually running; the exporter blocks at shutdown waiting for one. |
NASO_DARKWEB_TOR_CONTROL_PASSWORD | the workers | Must match the TOR_CONTROL_PASSWORD build arg of the Tor images. |
PYTHONPATH, PYTHONDONTWRITEBYTECODE | the runtime | Set in docker-compose.yml. |
Verifying a configuration change
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.