Configuration¶
Every option in settings.json and the deployment .env.
Configuration Guide — Suspicious Platform¶
This document describes every configuration file and environment variable required to deploy and run Suspicious.
Configuration Files Overview¶
| File | Location | Purpose |
|---|---|---|
.env |
deployment |
Docker Compose runtime: ports, paths, credentials, versions |
settings.json |
Suspicious/ |
Main application: branding, behavior, integrations, mail |
config.json |
email-feeder/ |
Email ingestion: mailbox connectors, storage, polling, notifications |
make initchecks for these files and — if missing — creates them from sample templates (.env.example,settings-sample.json,config-sample.json). It also verifies directory structure, permissions, and certificates.Tested baseline. The full 13-service stack was brought up end-to-end with the image versions in §1.1. Four settings decide whether it starts and serves traffic — get these right first:
Setting Rule settings.jsondatabase.passwordmust equal .envMYSQL_PASSWORD(no Vault) — §2.5settings.jsonapp.debugtruefor plain HTTP onlocalhost;falseforces HTTPS via Traefik — §2.1settings.jsonapp.allowed_hostsinclude DOMAIN_CORP, or the Traefik path returnsDisallowedHost— §2.1.envCORTEX_PATHabsolute path when running Cortex — §1.7
1. .env — Deployment Environment¶
cp .env.example .env
1.1 Application Versions¶
SUSPICIOUS_VERSION=latest # `latest` tracks main; pin a tag for prod
# UI + feeder default to SUSPICIOUS_VERSION when unset; pin independently if needed.
SUSPICIOUS_UI_VERSION=
SUSPICIOUS_FEEDER_VERSION=
DB_SUSPICIOUS_VERSION=11.4
RUSTFS_VERSION=1.0.0-alpha.90
CORTEX_VERSION=4.1.0-1
ELASTICSEARCH_VERSION=8.19.7
TRAEFIK_VERSION=v3.3.4
CHROMADB_VERSION=0.6.3
REDIS_BROKER_VERSION=9-alpine
REDIS_CACHE_VERSION=9-alpine
This set was brought up together successfully. Only change versions when you know they are compatible — mismatched versions can break services.
1.2 Service Ports¶
SUSPICIOUS_PORT=9020
SUSPICIOUS_UI_PORT=9021
MINIO_PORT=35000 # MinIO / RustFS console — local access only
CORTEX_PORT=9001
1.3 Network Configuration¶
DOMAIN_CORP=your.corporate.domain
NETWORK_NAME=suspicious_net
NETWORK_SUBNET=172.20.0.0/16
NETWORK_GATEWAY=172.20.0.1
NETWORK_IP_RANGE=172.20.0.0/24
DOMAIN_CORP is used by Traefik for TLS/Host routing. Adjust network settings only if you need to avoid conflicts with existing Docker networks.
1.4 Database Credentials¶
MYSQL_DATABASE=db_suspicious
MYSQL_USER=suspicious
MYSQL_PASSWORD="your_db_user_password"
MYSQL_ROOT_PASSWORD="your_db_root_password"
⚠️ Must be set before first startup. MariaDB applies these only when it initialises an empty data volume. Changing them later — or reusing a volume created with different credentials — gives
Access denied for user 'suspicious'. To reset for a fresh start (this erases all data):docker compose rm -sf db_suspicious && docker volume rm suspicious_db_suspicious_data.
MYSQL_PASSWORDmust matchsettings.json→database.password(see §2.5).
1.5 MinIO / RustFS Credentials¶
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD="your_minio_password"
Used by both Suspicious and Email-Feeder to store attachments, artifacts, and processed data.
1.6 Container Names¶
DB_CONTAINER=db_suspicious
WEB_CONTAINER=suspicious
1.7 Local Paths¶
ROOT_PATH=../
SUSPICIOUS_PATH=../Suspicious
SUSPICIOUS_UI_PATH=../suspicious-ui
FEEDER_PATH=../email-feeder
DOCKER_PATH=../docker
YARA_PATH=../yara-rules
MISP_PATH=../misp
CORTEX_PATH=/opt/suspicious/cortex # ABSOLUTE when running Cortex (see note)
AIANALYZER_PATH=../Analyzers/AIMailAnalyzer
CA_PATH=./certificates
TRAEFIK_PATH=../traefik
CORTEX_PATHmust be an absolute path when the Cortex service runs. Cortex mounts analyzer job directories into sibling containers through the Docker socket, so the host and in-container paths must be identical. A relative value fails at startup withinvalid mount path: '../cortex/jobs' mount path must be absolute. A relative path is fine if you do not run Cortex (core dev stack).
1.8 Proxy Settings¶
HTTP_PROXY=
HTTPS_PROXY=
NO_PROXY=localhost
Leave blank unless your environment requires an outbound proxy. These values are
also passed as build args to docker compose build, so set them before
building images on a proxy-only network — otherwise the build fails with a
uv pip install (PyPI) or npm connection timeout.
2. settings.json — Application Configuration¶
Created by make init from settings-sample.json if absent.
2.1 Core Application¶
"app": {
"name": "suspicious",
"debug": false,
"secret_key": "CHANGE_ME",
"allowed_hosts": ["suspicious"],
"csrf_trusted_origins": ["https://suspicious.test"],
"timezone": "Europe/Paris",
"log_level": "INFO"
}
| Key | Description |
|---|---|
secret_key |
Django secret key. Generate with python3 -c "import secrets; print(secrets.token_urlsafe(64))". The boot-time schema check rejects placeholder/short keys; use 50+ random characters. |
debug |
false in production. Set true for local development — with false, Django redirects to HTTPS, so http://localhost:9020 is unreachable without Traefik. |
allowed_hosts |
Hostnames Django responds to (it always adds localhost and 127.0.0.1). For the Traefik/HTTPS path, include your DOMAIN_CORP or requests return DisallowedHost. |
csrf_trusted_origins |
Full origins allowed to POST (scheme + host), e.g. http://localhost:9020 for local and https://<DOMAIN_CORP> for the proxy. |
timezone |
Django timezone — affects timestamps and scheduled tasks |
log_level |
DEBUG / INFO / WARNING / ERROR |
2.2 Branding & UI¶
"branding": {
"company_name": "Test",
"contact_email": "suspicious@test.com",
"footer": "Your Group",
"intranet_link": "https://intranet.local",
"assets": {
"logo": "BASE64_LOGO",
"icon": "BASE64_ICON",
"banner": "BASE64_BANNER",
"signature": "BASE64_SIGNATURE"
}
}
Assets accept either a data:image/...;base64,... string or an https:// URL. These values drive the web UI appearance.
2.3 Features¶
"features": {
"dual_storage_write": false
}
| Key | Description |
|---|---|
dual_storage_write |
Write artifacts to both local storage and MinIO simultaneously |
2.4 URL Analysis¶
"url_analysis": {
"enabled": true,
"max_per_domain": 5,
"reuse_ttl_days": 7,
"shortener_domains": ["bit.ly", "tinyurl.com", "t.co", "goo.gl", "ow.ly"],
"suspicious_tlds": ["zip", "mov", "xyz", "top", "click"]
}
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Kill-switch for the URL analysis planner. Set false to disable URL planning globally without removing configuration. |
max_per_domain |
int | 5 |
Maximum number of URLs analyzed per registered domain per case. Lowest-interestingness URLs beyond this cap are marked SKIPPED. |
reuse_ttl_days |
int | 7 |
Cross-case result reuse window in days. A URL whose canonical key matches a prior URL with a fresh AnalyzerReport within this window is marked REUSED instead of re-analyzed. |
shortener_domains |
list | ["bit.ly", "tinyurl.com", "t.co", "goo.gl", "ow.ly"] |
Known URL-shortener hostnames. Matching URLs receive a +30 interestingness boost because their final destination is unknown. Overrides the built-in list when non-empty. |
suspicious_tlds |
list | ["zip", "mov", "xyz", "top", "click"] |
TLDs associated with phishing and abuse. URLs on these TLDs receive a +20 interestingness boost. Overrides the built-in list when non-empty. |
2.5 Database¶
"database": {
"engine": "mysql",
"host": "db_suspicious",
"port": 3306,
"name": "db_suspicious",
"user": "suspicious",
"password": "password",
"root_password": "strongpassword",
"options": {
"ssl": false,
"connection_pooling": false,
"persistent_connections": false
}
}
host, name, and user must match the .env MYSQL_* values. Without Vault,
password here must equal .env MYSQL_PASSWORD — the backend reads the
database password from settings.json at boot when VAULT_ADDR is unset. A
mismatch gives Access denied for user 'suspicious'.
2.6 Storage (MinIO / RustFS)¶
"storage": {
"backend": "local",
"s3": {
"endpoint": "rustfs:9000",
"public_endpoint": "localhost:35002",
"access_key": "MINIO_ACCESS_KEY",
"secret_key": "MINIO_SECRET_KEY",
"secure": false,
"auto_create_bucket": true,
"media_bucket": "suspicious-media",
"fast_metadata": false
}
}
"backend": "local" (the default) stores files on the container filesystem and
does not require the RustFS service — handy for a minimal dev run. Set
"backend": "s3" to use object storage; then secret_key here must match the
RustFS credentials in .env (MINIO_ROOT_PASSWORD), and RustFS must be running.
endpoint is the Docker-internal host the app container uses for
put/get/bucket calls. Presigned URLs (mail-attachment links, uploaded
avatar photos) are opened directly by the browser, which can't resolve that
internal hostname — set public_endpoint to whatever host:port the browser
can reach. Omit it and it falls back to endpoint — existing
single-endpoint deployments are unaffected.
Behind Traefik (security-headers middleware in
traefik/dynamic/tls.yaml sets Content-Security-Policy: img-src 'self'
data:), a public_endpoint on a different origin than the app — a bare
host:port — gets silently dropped by the browser's CSP enforcement; no
console-visible network error, the <img> just never loads. Two ways to
avoid it:
- Same-origin (recommended, no CSP change): set public_endpoint to
whatever origin users actually type into the address bar — self in CSP
terms means the browser's origin, not DOMAIN_CORP. They're usually
the same, but not always: a cert issued for DOMAIN_CORP still lets a
browser load the app over https://localhost (or any other name that
resolves to the same box) if it's been told to tolerate the mismatch,
and CSP then evaluates against localhost, not DOMAIN_CORP. Check
Traefik's access.log (the Referer column on any request) if unsure
which origin is actually in play. Then set public_secure = true and
route S3 GETs through Traefik under that origin — see the
suspicious-storage router in tls.yaml, which matches PathPrefix on
the configured bucket names (S3 path-style URLs put the bucket name
first in the path, so no prefix-stripping is needed) and forwards to
rustfs:9000 with passHostHeader: true (required — S3 SigV4 signs the
host header, so whatever Host the browser sent when the URL was
signed must reach rustfs unchanged, or signature verification fails).
Keep that router's bucket-name list in sync with avatars_bucket /
media_bucket / feeder_bucket.
- No Traefik in front (plain-HTTP dev stack): there's no CSP at all in
that path, so a bare localhost:<published-9000-port> public_endpoint
works — see the /deploy-full-e2e flow in CLAUDE.md.
The client that signs public_endpoint URLs pins region to a fixed value
("us-east-1" unless storage.s3.region overrides it) instead of letting
the SDK auto-detect it. Auto-detection does a live bucket-location request
against the endpoint being signed for — and the whole point of
public_endpoint is that it's reachable from a browser, not necessarily
from wherever the Django/Celery process signing the URL runs, so that
request can't be relied on to succeed.
DB overrides settings.json at runtime. storage.s3 (like other
sections) is seeded into the RuntimeConfig table on first run and served
from there — Redis-cached — ahead of settings.json. Editing the file
after seed_config has already run does nothing until the matching DB
row is updated too (Settings page in the UI, or
RuntimeConfig.objects.filter(key="storage.s3") in a shell) and the cache
entry is invalidated (settings.config.invalidate_cache("storage.s3")).
Feeder fast metadata¶
fast_metadata (default false) controls the email-ingestion fast path. The
email-feeder always writes a per-email email.json (parsed addresses, subject,
body, headers, attachment sha256) next to each .eml. When fast_metadata is
true, the backend builds its EmailDataModel directly from that JSON —
sha256-verified against the on-disk attachments — instead of re-opening and
re-parsing the .eml. Any validation error, sha256 mismatch, or missing field
falls back to parse_email, so the worst case equals the legacy behavior.
| Value | Behavior |
|---|---|
false (default) |
Backend always re-parses the .eml (legacy). |
true |
Backend reads email.json; falls back to parse_email on any mismatch. |
Enable only after end-to-end parity (fast path vs parse_email produce
identical Mail rows) is green in staging.
2.7 Integrations¶
Cortex (required for analyzers)¶
"cortex": {
"url": "http://cortex:9001",
"api_key": "CHANGE_ME",
"webhook_secret": "CHANGE_ME_LONG_RANDOM_STRING",
"stale_job_timeout_seconds": 86400,
"analyzers": {
"header": "MailHeader_4_0",
"ai": "AI_Mail_Analyzer_1_4",
"sandbox": "ThreatGridOnPrem_1_0",
"yara": "Yara_Boosted_3_2",
"file_info": "FileInfo_8_0"
}
}
| Key | Type | Default | Notes |
|---|---|---|---|
url |
string | — | Cortex API base URL. |
api_key |
string | — | Cortex API key. Generate via Cortex → Organization → User → API keys. |
webhook_secret |
string | "" (disabled) |
Bearer token Cortex sends to POST /api/cortex/webhook/. Compared via hmac.compare_digest to defeat timing oracles. Empty → webhook returns 503; configure Cortex with the matching shared secret. |
stale_job_timeout_seconds |
int | 86400 (24 h) |
CaseAnalyzerJob rows pending past this window are auto-failed by the fail_stale_jobs Celery task every 600 s, so cases never stall when Cortex drops a delivery. |
analyzers |
object | — | Analyzer names must match those installed in your Cortex instance, exactly. |
The webhook view writes cortex_job_processed:<jobId> into the Redis cache with a 1 hour TTL on first delivery; duplicate Cortex retries short-circuit there. The cron fallback (update_ongoing_cases, every 300 s) covers any delivery that is missed entirely.
ChromaDB (AI vector store)¶
"chromadb": {
"url": "http://chromadb:8000",
"host": "chromadb",
"port": 8000,
"collection_name": "suspicious_mails",
"ssl_verify": false
}
Used by the AI analyzer for semantic similarity search across processed mails.
TheHive (optional)¶
"thehive": {
"enabled": false,
"url": "https://thehive",
"api_key": "CHANGE_ME",
"verify_ssl": false,
"custom_field": "",
"email_sender": "",
"tags": "",
"certificate_path": "/app/cert.pem",
"user": "exemple@user.com"
}
Enable to automatically create cases/alerts in TheHive from Suspicious verdicts.
Watcher (optional)¶
"watcher": {
"enabled": false,
"url": "https://watcher",
"api_key": "CHANGE_ME",
"timeout": 10,
"verify_ssl": false
}
MISP (optional)¶
"misp": {
"default_tags": {
"tlp": "clear",
"pap": "clear",
"categories": ["MalSpam", "Phishing"]
},
"instances": {
"primary": {
"url": "http://misp",
"api_key": "CHANGE_ME",
"ssl_verify": false,
"ssl_ca_certs": "/etc/ssl/certs/ca-certificates.crt"
},
"secondary": {
"url": "https://secondary-misp",
"api_key": "CHANGE_ME",
"ssl_verify": false,
"ssl_ca_certs": "/etc/ssl/certs/ca-certificates.crt"
}
}
}
Allows pushing indicators of compromise to one or more MISP instances.
2.8 Authentication¶
"authentication": {
"oidc": {
"server_url": "https://oidc-server",
"client_id": "client-id",
"client_secret": "client-secret"
},
"ldap": {
"server_uri": "ldaps://ldap",
"bind_dn": "ou=Applications,ou=Gresources,o=Group",
"bind_password": "CHANGE_ME",
"base_dn": "ou=People,o=group",
"filter": "(&(mail=%(user)s)(Tpresent=true)(!(ou=admin)))",
"verify_ssl": true
}
}
Configure either OIDC or LDAP (or both).
LDAP TLS verification. verify_ssl defaults to true (OPT_X_TLS_DEMAND — full cert + hostname check). Setting it to false downgrades to OPT_X_TLS_NEVER and exposes bind credentials to any attacker on the network path; the boot logger emits a WARNING so the downgrade is visible in make logs. Use false only in local dev against a self-signed test LDAP, never in production. To support an internal CA, mount the CA bundle into the container and point libldap at it via the system trust store rather than disabling verification. This setting governs both LDAP code paths below.
Two independent LDAP query paths. This filter field (default (uid=%(user)s)) only controls the login search — django_auth_ldap's AUTH_LDAP_USER_SEARCH, used when a user authenticates. A second, separate query — the periodic profile sync in profiles/profiles_utils/ldap.py (Ldap.get_search_results, run every 10 min by the sync_user_profiles Celery beat task for every active user) — is not driven by this config field at all. It uses a filter hardcoded in that module, matching on mail, and further restricted by two attributes (Tpresent, TpreferredFirstName) that are not part of any standard LDAP schema (core/cosine/inetOrgPerson) — they assume a directory-specific schema extension. Against a plain-vanilla test LDAP server without that extension, this search silently returns zero results (not an error) for every user, so profile sync, CISO auto-detection, and directory-derived group membership will appear to do nothing. If you're standing up a local test directory (e.g. osixia/openldap) to exercise this path, either extend its schema to define those two attributes or drive Ldap.process_search_results() directly with hand-built search results in the shape python-ldap would return.
2.9 Company Domains¶
"domains": ["testgroup.com"]
Used to identify internal senders, auto-create users, and reduce false positives on domain matching.
2.10 Email & Notification¶
Controls SMTP settings, email content, links, social icons, and per-template logos.
"email": {
"api_base": "https://suspicious.test/api/",
"smtp": {
"server": "smtp.server.local",
"port": 25,
"username": "smtp_user",
"password": "smtp_password",
"tls": true
},
"content": {
"footer": "Limited Distribution",
"team_name": "Your Cybersecurity Team",
"global_domain": "test.com",
"website": "https://www.test.com/en"
},
"links": {
"submissions": "https://suspicious.test/submissions",
"security_contact": "mailto:security@test.com",
"security_text": "security@test.com",
"inquiry": "mailto:inquiry@test.com",
"inquiry_text": "inquiry@test.com",
"glossary": "https://glossary.local"
},
"socials": {
"facebook": "https://fr-fr.facebook.com/test",
"twitter": "https://x.com/test",
"instagram": "https://www.instagram.com/test",
"linkedin": "https://www.linkedin.com/company/test",
"youtube": "https://www.youtube.com/test"
},
"templates": {
"acknowledgement": "Suspicious – Submission Registered",
"review": "Your submission n°{case_id} has been reviewed as: {result}",
"final": "SUSPICIOUS EMAIL ANALYSIS - Your analysis [{case_id}] is completed"
},
"logos": {
"company": "data:image/png;base64,BASE64_LOGO",
"acknowledge": "data:image/png;base64,BASE64_LOGO",
"final": "data:image/png;base64,BASE64_LOGO",
"challenge": "data:image/png;base64,BASE64_LOGO",
"modif": "data:image/png;base64,BASE64_LOGO"
}
}
Key fields¶
| Key | Description |
|---|---|
api_base |
Base URL used to build challenge/portal links inside emails |
smtp.tls |
Enable STARTTLS on the SMTP connection |
content.team_name |
Team name shown in email body and footer |
content.global_domain |
Domain shown as a global link in the email footer |
links.security_contact |
mailto: URI for the security team — used in Dangerous verdict emails |
links.glossary |
Link to your cybersecurity glossary, shown in all email footers |
links.inquiry |
mailto: URI shown in the footer for general questions |
templates.* |
Subject line templates. {case_id} and {result} are interpolated at send time |
logos.* |
Per-template logos. Accept data:image/png;base64,..., data:image/svg+xml;base64,..., or https:// URLs. Outlook-safe rendering is handled automatically |
2.11 Observability¶
"observability": {
"opentelemetry": {
"enabled": false,
"service_name": "suspicious",
"otlp_endpoint": "http://tempo:4318"
}
}
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable OTel tracing. Set true in production. |
service_name |
string | "suspicious" |
Service name shown in Tempo/Grafana. |
otlp_endpoint |
string | "http://tempo:4318" |
OTLP HTTP collector URL. Use http://tempo:4318 when Tempo runs in the monitoring Docker profile. |
Example production configuration:
{
"observability": {
"opentelemetry": {
"enabled": true,
"service_name": "suspicious-prod",
"otlp_endpoint": "http://tempo:4318"
}
}
}
To collect traces, set observability.opentelemetry.enabled to true (above)
and start the Tempo + Grafana stack manually from its compose files under
deployment/docker/monitoring/ (Grafana serves on port 3000). The old
make monitor-up target has been removed.
3. email-feeder/config.json — Email Ingestion Service¶
Created by make init from config-sample.json if absent.
3.1 Mail Connectors (IMAP / IMAPS)¶
"mail-connectors": {
"imap": {
"imap-dev": {
"enable": true,
"host": "localhost",
"port": 3143,
"login": "imap_user",
"password": "imap_password",
"mailbox_to_monitor": "INBOX"
}
},
"imaps": {
"imaps-dev": {
"enable": false,
"host": "localhost",
"port": 3993,
"login": "imap_user",
"password": "imap_password",
"certfile": "/path/to/dev/certfile.pem",
"keyfile": "/path/to/dev/keyfile.pem",
"rootcafile": "/path/to/dev/rootcafile.pem",
"mailbox_to_monitor": "INBOX"
},
"imaps-prod": {
"enable": false,
"host": "localhost",
"port": 3993,
"login": "imap_user",
"password": "imap_password",
"certfile": "/path/to/prod/certfile.pem",
"keyfile": "/path/to/prod/keyfile.pem",
"rootcafile": "/path/to/prod/rootcafile.pem",
"mailbox_to_monitor": "INBOX"
}
}
}
You can define as many named connectors as needed. Set "enable": false to
deactivate a connector without removing it.
The feeder needs at least one enabled connector. With every connector disabled it logs
No mailboxes were successfully set up … Exitingand the container restart-loops. This is expected when you run the stack without a mailbox to poll; it does not affect the backend or UI.
| Key | Description |
|---|---|
host / port |
IMAP(S) server address and port |
login / password |
Mailbox credentials |
certfile / keyfile / rootcafile |
Client certificate paths (IMAPS only) |
mailbox_to_monitor |
Folder to poll — usually INBOX |
3.2 Working Directory & Polling¶
"working-path": "/tmp/suspicious",
"timer-inbox-emails": 10
| Key | Description |
|---|---|
working-path |
Temporary directory for fetched emails, attachments, and processing queue |
timer-inbox-emails |
Polling interval in seconds |
3.3 MinIO Storage¶
"s3": {
"endpoint": "rustfs:9000",
"access_key": "minioadmin",
"secret_key": "minioadmin",
"secure": false
}
Must match .env MINIO_* values. Used to store attachments and extraction results.
3.4 Outgoing Mail (Notifications)¶
"mail": {
"tls": true,
"server": "smtp_server",
"port": 25,
"password": "smtp_password",
"username": "smtp_user",
"footer": "Limited Distribution",
"group": "Your Cybersecurity Team Name",
"suspicious_web": "https://suspicious.test/submissions/",
"company_name": "test.com",
"company_url": "https://www.test.com/en",
"socials": {
"facebook": "https://fr-fr.facebook.com/test",
"twitter": "https://x.com/test",
"instagram": "https://www.instagram.com/test",
"linkedin": "https://www.linkedin.com/company/test",
"youtube": "https://www.youtube.com/test"
},
"glossary": "https://glossary_to_cyber_terms",
"inquiry": "mailto:inquiry@yourcompany.com",
"inquiry_text": "inquiry@yourcompany.com",
"security": "mailto:security@yourcompany.com",
"security_msg": "security@yourcompany.com",
"logos": {
"company": "data:image/png;base64,BASE64_COMPANY_LOGO",
"acknowledge-badmail": "data:image/png;base64,BASE64_BADMAIL_BANNER"
}
}
This section configures the email-feeder container's standalone mail service (distinct from the main application's settings.json email section). It is used to send the bad-submission acknowledgement email when a forwarded message cannot be processed.
| Key | Description |
|---|---|
tls |
Enable STARTTLS |
server / port |
SMTP host and port |
username / password |
SMTP credentials |
group |
Team name shown in the email body |
suspicious_web |
Portal URL linked in the footer |
company_name |
Domain / company identifier shown in the footer |
company_url |
Link for the global team name in the footer |
glossary |
Cybersecurity glossary URL shown in the footer |
inquiry / inquiry_text |
Contact mailto: URI and display label |
security / security_msg |
Security team mailto: URI and display label |
logos.company |
Company logo — accepts data:image/png;base64,..., data:image/svg+xml;base64,..., or https:// URL. Outlook receives a text wordmark fallback automatically |
logos.acknowledge-badmail |
Hero banner image for the bad-submission email |
Runtime config + secrets (DB + Vault)¶
In production, configuration is split across three tiers; settings.json stays
the dev/CI fallback for both.
- Non-secret runtime config now lives in the database
(
settings.RuntimeConfig). It is seeded fromsettings.jsonbypython manage.py seed_config(run automatically bymake deploy), editable from the Django admin, and read in code viasettings.config.get_config/settings.config.get_section. When the DB has no seeded value, the accessor falls back tosettings.json. - Secrets (API keys, passwords, the Django secret key) live in HashiCorp
Vault (KV v2 at
suspicious/<dotted-key>, fieldvalue), read viasuspicious.secrets.get_secretusing AppRole auth. WhenVAULT_ADDRis unset, secrets fall back tosettings.json/settings.ci.json. LeaveVAULT_ADDRunset for local development — Compose defaults it to empty, so the backend uses thesettings.jsonfallback. Set it only once Vault is provisioned; otherwise the backend fails fast at boot trying to reach Vault. - Schema caveat:
settings.jsonmust still keep schema-valid dummies forapp.secret_keyanddatabase.passwordeven under Vault — the boot-time schema check requires them, and the real values are overlaid from Vault whenVAULT_ADDRis set.
See deployment/VAULT.md for the secret map, prod bring-up order, and the dev/CI path.
Config authority (shared config across services)¶
The config authority is a scoped HTTP endpoint that lets backend services pull their effective configuration at boot instead of maintaining separate copies of shared values.
Scopes¶
| Scope | What it holds |
|---|---|
shared |
storage.s3, all email.* sections, branding — values used by more than one service. Never requested directly; always merged into a real scope. |
backend |
Integration settings (Cortex, TheHive, MISP, ChromaDB, LDAP/OIDC) and auth config. |
feeder |
No feeder-specific keys yet; consumes shared (S3, SMTP, branding). |
manage.py seed_config seeds each settings.json section into its scope.
Endpoint¶
GET /api/config/{scope}/
Returns the effective config for scope: the shared sections merged with the
scope's own sections. Non-secret fields come from the DB; secret leaves are
overlaid from Vault. Response always carries Cache-Control: no-store, is
throttled at 30 req/min, and is never written to application logs.
Auth: Knox token whose user belongs to the Django group named after the scope. Superusers may read any scope.
# Example — fetch the feeder scope (requires TLS via Traefik in production)
curl -s -H "Authorization: Token <feeder-token>" \
https://suspicious.example.com/api/config/feeder/
Provisioning¶
# Seed config sections into the DB (already run by `make deploy`)
docker compose exec suspicious python manage.py seed_config
# Create the feeder service account and print its Knox token (run once)
docker compose exec suspicious python manage.py create_service_token feeder
create_service_token feeder creates a svc-feeder user in the feeder
group and prints a Knox token. Paste it into deployment/.env as
FEEDER_API_TOKEN.
Effect on the email feeder¶
The feeder now fetches S3 credentials, SMTP settings, and branding from
GET /api/config/feeder/ at boot when BACKEND_URL and FEEDER_API_TOKEN
are set. It writes a last-good cache (.config-cache.json, mode 0600) and
falls back to it on a backend blip; with no cache and no backend it exits
immediately. As a result, the storage, outgoing-mail, and branding
blocks have been removed from email-feeder/config.json — that file now
holds only mail-connectors, working-path, and polling settings
(see §3).
Best Practices¶
- Never commit secrets — use environment variable injection, Docker secrets, or a secrets manager in production.
- Enable SSL/TLS everywhere in production — IMAPS connectors, database, Cortex, MISP, TheHive.
- Customize before first run — set
secret_key,allowed_hosts,csrf_trusted_origins, and branding assets before exposing the stack. - Back up before changing credentials — modifying
MYSQL_*after first run requires dropping the database volume, which erases all data. - Run
make initbeforemake upon every fresh deployment to catch missing files and permission issues.
Launch¶
Once .env, settings.json, and config.json are complete, build and start,
then initialise the database on the first run (the backend stays unhealthy
until migrations are applied):
docker compose build suspicious suspicious_ui
make up
make migrate
make seed-config
make createsuperuser
The full stack starts: web UI, API, database, email-feeder, Cortex, RustFS,
Elasticsearch, ChromaDB, Traefik, and Vault. make seed-config loads the
non-secret runtime config into the database (see Runtime config + secrets).
See INSTALL.md for the step-by-step walkthrough and verification.
Related Documentation¶
| Document | Description |
|---|---|
| INSTALL.md | Full installation and deployment instructions |
| deployment/README.md | Deployment-specific instructions |
| deployment/VAULT.md | Vault secrets + DB runtime-config runbook |
| README.md | Project overview, features, and usage |
| CONTRIBUTING.md | Development and contribution guidelines |