TheHive Flow Security#
6.0 One
TheHive Flow generates and stores several secrets on its host, isolates its services on an internal Docker network, and terminates TLS at the nginx entry point.
Docker Compose deployment
This page describes the Docker Compose deployment of TheHive Flow. On a Kubernetes deployment, see Configuration and operations on Kubernetes.
Secrets inventory#
| Secret | Location on host | Generated by | File permissions |
|---|---|---|---|
| PostgreSQL superuser password | .env |
init.sh |
600 |
orchestrator database password |
.env |
init.sh |
600 |
temporal database password |
.env |
init.sh |
600 |
temporal database password copy |
temporal/.resolved.yaml |
init.sh |
644 |
| JWT signing key | .env |
init.sh |
600 |
| S3 secret access key | .env |
init.sh |
600 |
| TheHive API key | orchestrator/secret/thehive-api-key |
Operator | 644 |
| TLS private key | nginx/certs/server.key |
init.sh or operator |
640 |
| TLS certificate | nginx/certs/server.crt |
init.sh or operator |
644 |
| S3 secret access key copy | backups/backup-s3-*.tar.gz |
backup.sh |
600 |
| Secret variables copy | backups/backup-orchestrator-*.pgdump |
backup.sh |
600 |
All generated secrets are 256-bit random values produced by openssl rand -hex 32. None are committed to git: .env, temporal/.resolved.yaml, and orchestrator/secret/* are all covered by .gitignore.
The mode-644 entries temporal/.resolved.yaml and orchestrator/secret/thehive-api-key can't be 600: each file is bind-mounted with its host ownership preserved into a container whose service runs as an unprivileged user, UID 1000 for temporal and UID 65532 for orchestrator, which reads it through the world-readable bit. A mode-600 file owned by the operator makes the service fail at startup. On a host shared with other users, provide the API key through BEEFLOW_SECRET_THEHIVE_API_KEY in .env instead, at mode 600.
Organization secret variables#
Organization secret variables live in the PostgreSQL database, not on the host file system. Their values are never displayed in the interface and never appear in execution logs, and a workflow can't reference a secret in a value that is stored or returned, such as a run label or a workflow output. Their only copy outside the database is the database backup, covered in the warning below.
External secret managers#
Secrets don't have to live in .env. The application resolves every BEEFLOW_SECRET_* variable from its environment first and falls back to the file value, so an external secret manager can inject the JWT signing key, TheHive API key, S3 secret access key, and Docker registry credentials at deploy time: pass them through the environment: block of docker-compose.override.yml, or export them in the shell that runs docker compose, which takes precedence over .env. A variable set to an empty value counts as unset, and the file fallback engages.
POSTGRES_PASSWORD, TEMPORAL_DB_PASSWORD, and GRAFANA_ADMIN_PASSWORD aren't read by the application: Docker Compose interpolates them, with the same precedence of the shell environment over .env. ORCHESTRATOR_DB_PASSWORD is the exception: the orchestrator container receives it as an environment variable, and the application expands it into the connection string of orchestrator/orchestrator.yml, with the same shell-over-.env precedence. One secret always lands on disk regardless: temporal/.resolved.yaml holds the temporal database password, written by init.sh.
Backup artifacts carry secrets
The object storage keeps its own S3 identity, access key and secret key in cleartext, inside the blob volume, so every archive of that volume carries a copy of BEEFLOW_SECRET_S3_SECRET_ACCESS_KEY. The database dump likewise carries the organization secret variables. The backup.sh and restore.sh scripts therefore create their artifacts with mode 600, including the .pre-restore-* safety copies. Treat ./backups/ with the same care as .env: encrypt it before shipping it off-host, and don't relax its permissions to make it easier to copy.
Network isolation#
orchestrator-netis an isolated Docker bridge. No port is reachable from the host unless explicitly declared withports:.- The
orchestratorservice on port 8081 isn't published on the host: all traffic enters through nginx on port 443. - The observability listener on port 9090 is published on loopback only,
127.0.0.1:9090, so operators can reach/livezand/readyzfrom the host. It has no application-level authentication: protect it with host firewall rules if the/metricsdata is sensitive. - PostgreSQL, Temporal, and the object storage aren't published on the host at all. The S3 API on port 8333 is reachable only from inside
orchestrator-net.
Webhook exposure#
Webhook triggers are the one entry point designed to be called by external systems. They enter through nginx on port 443, under the /webhook/ path prefix, and the authentication method configured on each webhook is what controls who can start the workflow: select one for any webhook reachable from outside your network.
Two network-level restrictions narrow the exposure further:
- Host firewall: When every caller is known, restrict inbound port 443 to their source addresses. TheHive reaches TheHive Flow on the same port, so keep TheHive host among the allowed sources.
-
Nginx: To restrict the webhook paths only, keeping the rest of the listener open to TheHive, add a dedicated location block to the nginx configuration template and restart nginx. Location blocks don't inherit the proxy directives, so the block repeats them, including the headers that carry the client IP the rate limiter reads:
location /webhook/ { allow <caller_cidr>; deny all; proxy_pass http://orchestrator:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; }
TLS#
Nginx terminates TLS on port 443. Two modes are available, selected by init.sh:
- Self-signed certificate, the default:
init.shgenerates a 365-day, 2048-bit RSA certificate for the configurednginx_server_name. Clients must either trust the certificate or turn off certificate verification. - Custom certificate: place
server.crt,server.key, and optionallyca.pemin./certificates/before runninginit.sh. The files are copied tonginx/certs/and thessl_trusted_certificatedirective is enabled automatically in the nginx configuration.
Internal service-to-service communication, from the orchestrator service to PostgreSQL, Temporal, and the object storage, uses plain TCP on the isolated Docker network. The S3 endpoint is plain HTTP. No mTLS is used between internal services in this release.
TLS toward TheHive#
When ORCHESTRATOR_THEHIVE_URL is an https:// address, the orchestrator service validates TheHive certificate against the system trust store of its image, so a certificate signed by a public CA works as is. There's no option to turn off certificate verification on this channel.
For a certificate signed by an internal CA, mount the CA bundle into the container and point the SSL_CERT_FILE variable at it in docker-compose.override.yml, then apply with docker compose up -d orchestrator:
services:
orchestrator:
volumes:
- /path/to/internal-ca.pem:/etc/ssl/internal-ca.pem:ro
environment:
SSL_CERT_FILE: /etc/ssl/internal-ca.pem
The file replaces the system trust store for the whole process, so it must hold every CA the service needs to trust, including the public CAs used by the external systems your workflows call.
Rotate secrets#
There's no scheduled rotation, and no tooling automates one: every secret is generated once by init.sh and rotated manually with the procedures below.
JWT signing key#
The JWT key must be the same on TheHive Flow and TheHive simultaneously. In-flight requests fail during the brief rotation window.
- Generate a new key:
openssl rand -hex 32 - Update
JWT_SIGNING_KEYin.env. - Update the TheHive configuration with the new key.
-
Apply the change on both sides at the same time. Recreate the containers rather than restarting them, because a plain
docker compose restartdoesn't re-read.env. On TheHive Flow host:docker compose up -d orchestratorOn TheHive host:
docker compose up -d thehive
TheHive API key#
Update the value in .env:
BEEFLOW_SECRET_THEHIVE_API_KEY=<thehive_api_key>
Alternatively, update the secret file:
printf '%s' '<thehive_api_key>' > ./orchestrator/secret/thehive-api-key
chmod 644 ./orchestrator/secret/thehive-api-key
To apply the change, the command depends on where you put the key. After editing .env, recreate the container, because a plain restart doesn't re-read .env:
docker compose up -d orchestrator
After editing the secret file, restart the service so the file is read again:
docker compose restart orchestrator
PostgreSQL passwords#
Rotating a database password requires stopping the stack and altering the database role.
-
Stop the services:
docker compose stop orchestrator temporal -
Alter the database role:
docker compose exec postgresql psql -U postgres -c \ "ALTER ROLE orchestrator PASSWORD '<new_password>';" -
Update
ORCHESTRATOR_DB_PASSWORDin.env. -
Start the services again:
docker compose start orchestrator temporal
TLS certificate renewal#
For the self-signed certificate, which expires after 365 days:
rm ./nginx/certs/server.crt ./nginx/certs/server.key
bash ./scripts/generate_certs.sh
docker compose restart nginx
For a custom certificate, replace the files in ./nginx/certs/, then run docker compose restart nginx. No other service downtime is required.
S3 secret access key#
The key exists in two places that must agree: .env, which the orchestrator service reads, and the S3 identity stored inside the object storage itself, which init-s3-store provisioned. Updating only .env leaves TheHive Flow authenticating with a key the storage rejects.
Update BEEFLOW_SECRET_S3_SECRET_ACCESS_KEY in .env, then provision the identity on the store again and recreate the orchestrator container so it picks up the new value:
docker compose up -d init-s3-store
docker compose up -d orchestrator
up -d runs the provisioning container again even though it has already run and exited: Compose restarts a stopped one-shot service, and recreates it outright when its environment changed, which is the case here. Stored blobs are unaffected, because the identity governs access, not the content.
Rotating the secret doesn't revoke the old one. The provisioning step adds or updates a credential under the identity and never removes credentials you stop referencing. In particular, changing S3_ACCESS_KEY_ID rather than just the secret leaves the previous access key still active on the store indefinitely. Changing only BEEFLOW_SECRET_S3_SECRET_ACCESS_KEY while keeping the same access key ID replaces that key's secret, which is the rotation path the stack supports. To retire an access key ID entirely, remove it explicitly:
The following command drops the identity and every access key under it. The -apply flag is required: without it, s3.configure runs in simulation mode and changes nothing.
docker compose exec s3-store \
sh -c 'echo "s3.configure -user orchestrator -delete -apply" | weed shell -master s3-store:9333'
Then provision the identity you want to keep:
docker compose up -d init-s3-store
Verify what the store actually holds before and after. The output contains secret keys in cleartext, so don't paste it into a ticket:
docker compose exec s3-store \
sh -c 'echo "s3.configure" | weed shell -master s3-store:9333'
Grafana admin password#
Only used by the bundled observability stack. The password is randomized in GRAFANA_ADMIN_PASSWORD, generated by init.sh. The username stays admin, the fixed Grafana default. To rotate:
Update GRAFANA_ADMIN_PASSWORD in .env, then recreate the container:
docker compose up -d grafana
Grafana only applies the admin password when it bootstraps the admin user for the first time, so a plain docker compose restart grafana doesn't work: restart reuses the existing container without re-reading .env, and Grafana ignores the value once the admin user exists. up -d recreates the container instead. Since the Grafana service has no persistent volume, and dashboards and rules are always provisioned from files, the recreated container has no memory of the old admin user and bootstraps a fresh one from the new password.