Database encryption
Netzilo Server can encrypt the sensitive columns of its management database with a key held in a key store you control: HashiCorp Vault, AWS KMS, Azure Key Vault, or a key in the configuration file. A copy of the database, such as a dump, a backup, a replica or a disk snapshot, then holds ciphertext for those columns, and reading them needs the key store as well.
Database encryption is off unless you turn it on. Nothing turns it on by itself, and an upgrade never does. Netzilo Cloud runs with it on.
This page shows what it protects, how to set up each key store, how to turn it on for a
server installed with the one-line installer or from the AWS or Azure Marketplace image,
and how to operate and troubleshoot it. Every command runs on the server, as root or with
sudo, from the compose folder ($C, see
Server reference).
What it protects
It protects against anyone who gets the management database without the key store: a lost or stolen dump or backup, a database replica, a disk or volume snapshot, a database administrator who is not a Netzilo administrator.
It does not protect against someone who controls the running server: the management
container, or the host with management.json and the key store access the server has. The
server needs these values to work and reads them. The dashboard and the REST API show them
as before, masked where they were masked.
What is encrypted:
| Data | Where it appears in the dashboard |
|---|---|
| Setup keys, and the setup key each peer enrolled with | Endpoint > Setup Keys, peers |
| Personal access tokens and service-user tokens | Team > Users and Team > Agents, tokens |
| AI Gateway provider credentials | Edge > Providers |
| MCP server environment, headers and OAuth settings; users' MCP sign-ins and header values | Edge > Providers, MCP servers |
| AI provider API keys | Integrations > Artificial Intelligence |
| Event streaming and integration settings (their secrets) | Integrations |
| Profile components (Workspace and browser encryption keys, enrollment keys) | Endpoint > Profiles |
| AI chat messages and AI session event content | Edge > Sessions |
| The activity log's initiator name, initiator email and details | Activity > Events |
| AI Assistant conversations, summaries, answers and steps | the AI Assistant |
What stays readable in the database, by design: object names and ids, peers and their addresses, groups, policies, routes, timestamps, the event type of each activity, and the AI facts used for counting (agent, rule, model, server, tool, source, surface). Searching events keeps working: keyword search uses keyed tokens of the words, so a dump shows tokens, not words, and filtering by user works.
The identity provider's database (zitadel) is not part of this; it is encrypted with its
own master key. DataStoreEncryptionKey in management.json is a separate setting and is
not used by this feature.
How it works
- Two keys. A random 256-bit data key encrypts the columns. The key store's key wraps
the data key, and only the wrapped form is stored, in the table
encryption_keysof the same database. The key store's key never leaves the key store. - The key store is used at start, not per request. Management unwraps the data key once when it starts and keeps it in memory. A key store outage does not affect a running server; it stops the next start. Management retries an unreachable key store for about 30 seconds, then exits, and the container restarts and tries again. A definite refusal, such as permission denied or a disabled or deleted key, is not retried.
- Two value formats. Most columns use AES-256-GCM with a fresh nonce per value; long text is compressed first, so the event and chat tables usually get smaller. The few columns the server looks values up by (setup key, token hash, event initiator email) use deterministic AES-SIV: equal values give equal ciphertext, which a dump reveals, and nothing else. Every value is bound to its table and column, so a value copied into another column does not decrypt.
- Turning it on encrypts existing data. At the first start with encryption on, management encrypts the small tables (setup keys, tokens, credentials, peers, profiles) in seconds before it serves, then the large ones (events, AI messages, AI Assistant history) in the background while it serves, at roughly a thousand rows a second. Both forms are read until the pass finishes. Progress is kept, so a restart continues where it stopped.
- Redis. Cache entries that carry stored objects are encrypted with the same key. There is nothing to do on the Redis side.
- Cost. Request latency and CPU with encryption on stay within the normal variation of the same server without it.
Choose a key store
| Key store | Provider | Use it when | What management signs in with |
|---|---|---|---|
| HashiCorp Vault (transit engine) | vault | on-premises, or any site that runs Vault | a Vault token, an AppRole, or Kubernetes auth |
| AWS KMS | awskms | the AWS Marketplace image, or any server with AWS access | the EC2 instance role, or AWS credentials in the environment |
| Azure Key Vault | azurekeyvault | the Azure Marketplace image, or any server with Azure access | the VM's managed identity, or a service principal in the environment |
| Local key | local | tests, or when none of the above is available | a base64 key in management.json |
local is the weakest choice: the key sits in management.json. It protects a database
dump only as long as that dump is never stored together with management.json. The backup
in Backup and restore copies both into one folder, so with
local, keep the key out of that backup and store it separately (see
Backups and restore).
Before you start
-
Back up. Turning encryption on rewrites rows; a backup taken before is your way back. Run the backup check and do not start if it fails.
-
Check that the running management has the feature:
cd "$C" && sudo docker compose exec -T management /go/bin/netzilo-mgmt encryption --helpA list with
status,decrypt,rewrapandkeygenmeans yes.unknown command "encryption"means you upgrade management first, see Servers installed earlier. -
Database. The bundled PostgreSQL, an external PostgreSQL, SQLite and the Redis-cached store are supported. The legacy JSON file store is not.
-
Agree on two rules with whoever runs the key store:
- the key store must be reachable from the server at every start;
- losing the key store's key (deleted, purged, or an old version disabled) makes the encrypted columns unreadable for good, in the database and in every backup. Turn on the key store's own deletion protection; each section below says how.
-
Every management instance that uses the database gets the same
EncryptionConfig. Several instances exist only in custom deployments.
Set up the key store
Do these steps from a workstation or from the server, with the key store's own tools. Each
section ends with an EncryptionConfig block that you add to management.json in
Turn it on.
HashiCorp Vault
export VAULT_ADDR=https://vault.example.com:8200
vault secrets enable transit # once per Vault
vault write -f transit/keys/netzilo-management # the key; aes256-gcm96 by default
vault policy write netzilo-management - <<'EOF'
path "transit/encrypt/netzilo-management" { capabilities = ["update"] }
path "transit/decrypt/netzilo-management" { capabilities = ["update"] }
EOF
Then choose how management authenticates:
| Method | Create it | Fields |
|---|---|---|
| Periodic token (simplest) | vault token create -orphan -policy=netzilo-management -period=768h | Token |
| AppRole | vault auth enable approle; vault write auth/approle/role/netzilo-management token_policies=netzilo-management token_ttl=1h token_max_ttl=24h; read the role id, create a secret id | RoleID, SecretID |
| Kubernetes (management on Kubernetes) | vault write auth/kubernetes/role/netzilo-management bound_service_account_names=<sa> bound_service_account_namespaces=<ns> policies=netzilo-management ttl=1h | KubernetesRole, and KubernetesJWTPath if the token is not at the default path |
Management renews a token every 15 minutes, and signs in again with AppRole or Kubernetes when a token expires.
"EncryptionConfig": {
"Mode": "on",
"Provider": "vault",
"Vault": {
"Address": "https://vault.example.com:8200",
"Token": "<token>",
"Mount": "transit",
"KeyName": "netzilo-management"
}
}
| Field | Default | Notes |
|---|---|---|
Address | required | Must be reachable from the management container. 127.0.0.1 there is the container itself, not the host |
Token, or RoleID and SecretID, or KubernetesRole | one is required | |
Mount | transit | The transit engine's mount path |
KeyName | netzilo-management | |
Namespace | Vault Enterprise namespace | |
CACert | Path inside the container of a PEM file to trust for Address. Mount it as shown in Turn it on |
Deletion protection. Keep the key's deletion_allowed false, which is the default, and
never raise its min_decryption_version past the version that wrapped the stored key. Back
up Vault itself: losing Vault's data loses the key.
AWS KMS
Create a symmetric key and an alias:
aws kms create-key --description "Netzilo Server database encryption" \
--key-usage ENCRYPT_DECRYPT --key-spec SYMMETRIC_DEFAULT --query KeyMetadata.Arn --output text
aws kms create-alias --alias-name alias/netzilo-management --target-key-id <key-arn>
aws kms enable-key-rotation --key-id <key-arn> # optional; transparent to management
Allow the server's identity to use the key. On the AWS Marketplace image that is the instance role the stack created. Add an inline policy to it:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt"],
"Resource": "<key-arn>",
"Condition": { "StringEquals": { "kms:EncryptionContext:netzilo": "netzilo-management-dek" } }
}]
}
The condition is optional. Management always sends that encryption context, so the condition limits the role to this use of the key.
The container and the instance role. Management runs in a container, one network hop further from the instance metadata service than the host. With IMDSv2 required, as on the AWS image, the hop limit must be at least 2 or the container gets no credentials:
aws ec2 modify-instance-metadata-options --instance-id <instance-id> \
--http-tokens required --http-endpoint enabled --http-put-response-hop-limit 2
"EncryptionConfig": {
"Mode": "on",
"Provider": "awskms",
"AWSKMS": { "Region": "us-east-1", "KeyID": "alias/netzilo-management" }
}
| Field | Notes |
|---|---|
Region | Always set it; it is not taken from the instance |
KeyID | Key id, key ARN, alias name (alias/…) or alias ARN |
Endpoint | Optional: a VPC endpoint or FIPS endpoint URL |
Outside AWS, put AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY of an IAM user limited to
the policy above in the management service's environment (step 4 of
Turn it on).
Deletion protection. Never schedule the key for deletion. KMS waits 7 to 30 days before
it deletes a key, and aws kms cancel-key-deletion undoes it in that time. A disabled key
stops every start of management until it is enabled again.
Azure Key Vault
Create a vault with role-based access and purge protection, and an RSA key that may only wrap and unwrap:
az keyvault create -g <resource-group> -n <vault-name> -l <region> --sku premium \
--enable-rbac-authorization true --enable-purge-protection true --retention-days 90
az keyvault key create --vault-name <vault-name> -n netzilo-management \
--kty RSA-HSM --size 3072 --ops wrapKey unwrapKey # --kty RSA on the standard SKU
az lock create -n netzilo-key-no-delete --lock-type CanNotDelete \
--resource "$(az keyvault show -g <resource-group> -n <vault-name> --query id -o tsv)"
Creating the key needs data-plane access to the vault from where az runs: the
Key Vault Crypto Officer role on the vault, and network access. Create the key before
you restrict the vault's network.
Allow the server's identity to use the key, and nothing else:
KEY_SCOPE="$(az keyvault show -g <resource-group> -n <vault-name> --query id -o tsv)/keys/netzilo-management"
az role assignment create --assignee-object-id <principal-id> --assignee-principal-type ServicePrincipal \
--role "Key Vault Crypto Service Encryption User" --scope "$KEY_SCOPE"
| Server | <principal-id> |
|---|---|
| Azure Marketplace image | The VM's system-assigned managed identity, which the image deploys with: az vm show -g <managed-resource-group> -n <vm-name> --query identity.principalId -o tsv |
| Another Azure VM | Turn on a managed identity with az vm identity assign, then the same |
| Kubernetes on AKS | A user-assigned identity federated with management's service account, see Custom deployments |
| Outside Azure | A service principal: az ad sp show --id <app-id> --query id -o tsv |
The role grants get, wrap and unwrap on that key. A new role assignment can take a few
minutes to apply; a start in that time fails with a 403 and the container retries.
Network. Management reaches the vault at https://<vault-name>.vault.azure.net. If you
close the vault's public access, give the server a path: a private endpoint in the server's
virtual network, with the privatelink.vaultcore.azure.net private DNS zone linked to it,
or a service endpoint on the server's subnet with that subnet allowed in the vault's
firewall.
"EncryptionConfig": {
"Mode": "on",
"Provider": "azurekeyvault",
"AzureKeyVault": { "KeyURL": "https://<vault-name>.vault.azure.net/keys/netzilo-management" }
}
Use the URL without a version. Management then wraps with the newest key version and, after a rotation, wraps the stored key again with the new version at its next start. A URL that ends in a version pins that version.
Management signs in with Azure's default credential chain: environment variables, workload
identity, then managed identity. Outside Azure, put AZURE_TENANT_ID, AZURE_CLIENT_ID and
AZURE_CLIENT_SECRET (or AZURE_CLIENT_CERTIFICATE_PATH) of the service principal in the
management service's environment (step 4 of
Turn it on).
Deletion protection. Purge protection and the delete lock above. Never disable or delete an older key version while a stored key or a backup may still be wrapped with it, see Rotate the key store's key.
Local key
Create a key:
cd "$C" && sudo docker compose exec -T management /go/bin/netzilo-mgmt encryption keygen
"EncryptionConfig": {
"Mode": "on",
"Provider": "local",
"Local": { "Key": "<output of keygen>" }
}
Keep a copy of the key somewhere other than the server and its backups. Without it the encrypted columns cannot be read.
Turn it on
Compose installs: one-line installer, AWS and Azure images
All three use the same compose layout; only the compose folder differs.
-
Do the steps in Before you start.
-
Find the compose folder and keep a copy of the current file:
if [ -f /opt/netzilo/run/docker-compose.yml ]; then C=/opt/netzilo/run; else C=/opt/netzilo; fi cd "$C" && sudo cp -a management.json "management.json.pre-encryption" -
Add your block from Set up the key store as a top-level key of
management.json, keeping everything else. For example, for Vault:sudo python3 - <<'PY' import json path = "management.json" cfg = json.load(open(path)) cfg["EncryptionConfig"] = { "Mode": "on", "Provider": "vault", "Vault": {"Address": "https://vault.example.com:8200", "Token": "<token>", "Mount": "transit", "KeyName": "netzilo-management"}, } with open(path, "r+") as f: # same file, same owner and mode f.seek(0); f.write(json.dumps(cfg, indent=2) + "\n"); f.truncate() PY sudo python3 -c 'import json; print(json.load(open("management.json"))["EncryptionConfig"]["Provider"])' -
Only if the key store needs something outside
management.json, such as cloud credentials outside their cloud or a CA file for Vault, add it to themanagementservice indocker-compose.yml:management: environment: # … the existing lines stay … - AZURE_TENANT_ID=<tenant-id> - AZURE_CLIENT_ID=<app-id> - AZURE_CLIENT_SECRET=<secret> volumes: # … the existing lines stay … - ./vault-ca.pem:/etc/netzilo/vault-ca.pem:ro # then "CACert": "/etc/netzilo/vault-ca.pem"sudo docker compose config -qmust print nothing. -
Recreate management. The dashboard and the API are unavailable for the seconds this takes; connected devices keep their tunnels.
sudo docker compose up -d --force-recreate management sudo docker compose logs -f management | grep -m 3 -i "database encryption"Expected, in this order:
database encryption: created data encryption key <id>, wrapped by <provider> database encryption on: key <id> wrapped by <provider> (<key reference>), 0 retired key(s) readable database encryption: encrypting setup_keys done, <n> rows in <time>One
encrypting <table> doneline follows per table;eventsandai_messagescome last and can take minutes. A start that fails logsfailed creating Store: … database encryption: …and the container restarts; see Troubleshooting. To back out, putmanagement.json.pre-encryptionback and recreate management again. Nothing was encrypted if no key was created. -
Check it, then check that management answers as described in Upgrade one component by hand.
Servers installed earlier
Upgrade first, then turn it on. Encryption needs a management release that has it.
Upgrade management the usual way, with encryption still off: update management (or the
full upgrade) on a one-line install, or a new management image reference on an AWS or
Azure image, see Upgrade and add components. Confirm with the check
in step 2 of Before you start, then follow the steps above.
Upgrades keep it on. The one-line installer's upgrade and update commands read
management.json and write it back with every key, so the EncryptionConfig block stays.
The marketplace procedure changes only image references. After every upgrade, before
you call it done:
cd "$C"
sudo python3 -c 'import json; print(json.load(open("management.json")).get("EncryptionConfig", {}).get("Mode"))' # on
sudo docker compose config | grep -E "AZURE_|AWS_|NETZILO_ENCRYPTION_" | sed 's/=.*/=…/' # the variables you added, if any
sudo docker compose logs --since 10m management | grep "database encryption on:"
| If an upgrade… | Result | Fix |
|---|---|---|
replaced management.json with one without the block | management refuses to start: the database holds encrypted values but EncryptionConfig.Mode is off | put the block back from management.json.pre-encryption or the backup, recreate |
| rolled management back to a release without encryption | the encrypted columns are unreadable to it | move forward again. To go back for good, turn it off first with the newer release |
| lost the variables you added in step 4 | the start cannot reach the key store | add them again, recreate |
Never re-run the installer as a fresh install on a working server: it erases the server, see Upgrade and add components. Recover with a restore and the same key store.
Custom deployments and Kubernetes
- Configuration. Mount
management.jsonwith the block from a Secret, or set the environment variables on the container. The file and the variables combine; a variable wins. - Identity without stored secrets. On AKS, workload identity: a user-assigned managed
identity with a federated credential for
system:serviceaccount:<namespace>:<service-account>and the role from Azure Key Vault on the key; the service account annotatedazure.workload.identity/client-id: <client-id>; the pod labelledazure.workload.identity/use: "true"and run under that service account. On EKS, IRSA: the service account annotated with an IAM role that has the policy from AWS KMS. With Vault,KubernetesRole. - Private networking. If the key store is reachable only privately, check that the cluster's DNS resolves it to the private address and that no NetworkPolicy blocks the traffic.
- Readiness. Give the management container a readiness probe on its listening port; a
TCP check is enough. Management opens the port only after the key is unwrapped, so a pod
that cannot reach the key store never becomes ready and a rolling update keeps the old
pod serving. Wait for the rollout in your pipeline (
helm upgrade --wait,kubectl rollout status) so a failure is visible. - Rolling updates. Roll out the release that has encryption first, with encryption
still off, then turn it on in a second rollout. An instance still running with encryption
off stops writing secrets as soon as another turns it on, and logs
another instance turned encryption on, until it is replaced. - Shutdown. Give the pod a termination grace period of at least 30 seconds.
Environment variables
Every field can come from the environment instead of the file; a variable that is set wins over the file.
| Variable | Field |
|---|---|
NETZILO_ENCRYPTION_MODE | Mode |
NETZILO_ENCRYPTION_PROVIDER | Provider |
NETZILO_ENCRYPTION_LOCAL_KEY | Local.Key |
NETZILO_ENCRYPTION_VAULT_ADDR | Vault.Address |
NETZILO_ENCRYPTION_VAULT_TOKEN | Vault.Token |
NETZILO_ENCRYPTION_VAULT_ROLE_ID | Vault.RoleID |
NETZILO_ENCRYPTION_VAULT_SECRET_ID | Vault.SecretID |
NETZILO_ENCRYPTION_VAULT_K8S_ROLE | Vault.KubernetesRole |
NETZILO_ENCRYPTION_VAULT_K8S_JWT_PATH | Vault.KubernetesJWTPath |
NETZILO_ENCRYPTION_VAULT_MOUNT | Vault.Mount |
NETZILO_ENCRYPTION_VAULT_KEY | Vault.KeyName |
NETZILO_ENCRYPTION_VAULT_NAMESPACE | Vault.Namespace |
NETZILO_ENCRYPTION_VAULT_CACERT | Vault.CACert |
NETZILO_ENCRYPTION_AWS_REGION | AWSKMS.Region |
NETZILO_ENCRYPTION_AWS_KEY_ID | AWSKMS.KeyID |
NETZILO_ENCRYPTION_AWS_ENDPOINT | AWSKMS.Endpoint |
NETZILO_ENCRYPTION_AZURE_KEY_URL | AzureKeyVault.KeyURL |
Check it
cd "$C" && sudo docker compose exec -T management \
/go/bin/netzilo-mgmt encryption status --count --log-file console
configured mode: on
stored key: 3f9c… wrapped by vault (https://vault.example.com:8200/v1/transit/keys/netzilo-management), created 2026-01-15 10:00:02
PASS DONE ROWS UPDATED
enabled true 0 2026-01-15 10:00:02
enc:setup_keys true 12 2026-01-15 10:00:03
…
running instances with encryption on: 1
TABLE PLAINTEXT ROWS ENCRYPTED ROWS
setup_keys 0 12
…
| Line | Healthy |
|---|---|
configured mode | on |
stored key | The provider and key you configured |
Every enc:<table> | DONE true once the background pass has finished |
running instances with encryption on | The number of management instances you run, usually 1 |
PLAINTEXT ROWS (with --count) | 0 for every table once the passes are done |
--count scans every table and takes a while on a large event table. Leave it out for a
quick look.
Day-to-day operations
Backups and restore
- Backups need no change. A dump holds the ciphertext and the wrapped data key. The backup in Backup and restore stays the same.
- A restore needs the key store. Restore the whole database and the same
management.jsonwith itsEncryptionConfig, and make sure the restored server reaches the key store with an identity that may use the key. A new VM or cluster has a new identity: grant it as in Set up the key store before the first start. The key store's key, or the key version that wrapped the data key when the backup was taken, must still exist and be enabled. - Restore the whole database, not single tables. The data key travels with the data in
encryption_keys. A database whose encrypted rows came back without that table makes management refuse to start, instead of making a new key. - A copy of production for testing reads the encrypted columns only with access to the same key store key.
- Local key. The backup copies
management.jsonwith the dump, which puts the key next to the data. Either set the key withNETZILO_ENCRYPTION_LOCAL_KEYfrom outside the compose folder, or store those backups as carefully as the key.
Rotate the key store's key
| Key store | Rotate | What management does |
|---|---|---|
| Vault | vault write -f transit/keys/netzilo-management/rotate | At its next start, wraps the data key again with the newest version |
| AWS KMS | automatic rotation, or aws kms rotate-key-on-demand | Nothing needed; KMS keeps the old key material |
| Azure Key Vault | az keyvault key rotate, or a rotation policy | With a KeyURL without a version, wraps the data key again with the newest version at its next start, and logs key … rewrapped from azurekeyvault key version … |
Keep older versions usable (Vault: do not raise min_decryption_version past them; Azure:
do not disable them) as long as you may restore a backup taken before the rotation.
Rotating the key store's key does not re-encrypt the data. To replace the data key itself, turn encryption off and on again; that creates a new data key.
Move to another key store or key: rewrap
rewrapChanging Provider, or the Vault, KMS or Key Vault key, in the configuration alone makes
management refuse to start with key provider mismatch. Move the wrapped data key instead;
the data is not touched:
cd "$C"
sudo cp -a management.json management.old.json
# edit management.json: the new EncryptionConfig; keep management.old.json as it was
sudo docker compose stop management
sudo docker compose run --rm --no-deps -T \
-v "$PWD/management.old.json:/etc/netzilo/management.old.json:ro" \
--entrypoint /go/bin/netzilo-mgmt management \
encryption rewrap --old-config /etc/netzilo/management.old.json --log-file console
# expect: rewrapped the key with <new provider>
sudo docker compose up -d management
Both key stores must be reachable from the server while the command runs. Keep the old key until the backups taken before the move have expired.
Turn it off: decrypt
decryptSetting Mode to off alone makes management refuse to start while the database holds
encrypted values. Decrypt in place first:
cd "$C"
sudo docker compose stop management
sudo docker compose run --rm --no-deps -T --entrypoint /go/bin/netzilo-mgmt management \
encryption decrypt --log-file console
# then set "Mode": "off" in management.json (or remove the block), and:
sudo docker compose up -d management
- The command needs the key store, so the provider settings must still be there. It waits up to two minutes for the stopped instance's last heartbeat to age out.
- While it runs, and if it is interrupted, no instance starts with encryption on. Run it
again to finish, or
encryption decrypt --cancelto abandon it and keep encryption on. --forcedecrypts even though an instance reported recently (that instance stops using the key within 30 seconds), and retires the key even if some values did not decrypt.- At the end the key is retired, not deleted: it stays in
encryption_keys, read-only. Turning encryption on again later creates a new data key.
What never to do
- Delete or disable the key store's key, or an older version of it that is still in use.
- Delete or edit rows of
encryption_keysorencryption_progress. - Change the provider or the key in the configuration without
rewrap. - Set
Modetooffwithoutdecrypt. - Restore single tables into an encrypted database, or copy rows between databases with different keys.
Troubleshooting
A start failure appears in sudo docker compose logs management as
failed creating Store: <data directory>: database encryption: <reason>. A configuration
error stops management earlier, names the field, and has no failed creating Store line.
| Message | Cause | Fix |
|---|---|---|
EncryptionConfig.Mode "…": must be on or off, EncryptionConfig.Provider is required when Mode is on, EncryptionConfig.<field> is required for the <provider> provider, EncryptionConfig.Vault needs Token, RoleID and SecretID, or KubernetesRole | the block is incomplete or misspelled. Field names are case-sensitive | correct management.json or the variable, recreate |
key provider <name>: … (retrying in …), then the container restarts | the key store is unreachable: DNS, firewall, private endpoint or proxy | test the address with curl -sv from the host and from the container's network, and fix the path |
the key provider denied the key: vault encrypt: 403 permission denied | the token or role lacks the policy | attach the policy from HashiCorp Vault |
the key provider denied the key: vault login: 400 … | the AppRole secret id or the Kubernetes role is wrong or revoked | create a new secret id, or fix the role binding |
x509: certificate signed by unknown authority (Vault) | Vault's certificate is not trusted | set CACert to a file mounted into the container |
the key provider denied the key: AWS KMS …: … is not authorized to perform: kms:Decrypt (or kms:Encrypt) | the role lacks the policy, or the encryption-context condition does not match | fix the policy from AWS KMS |
NoCredentialProviders or EC2RoleRequestError (AWS) | the container gets no instance credentials | set the metadata hop limit to 2, and check that the instance has a role |
MissingRegion (AWS) | Region is not set | set AWSKMS.Region |
the key provider denied the key: Azure Key Vault …: Forbidden | no role on the key yet, or a new role assignment still applying | assign the role from Azure Key Vault, and wait a few minutes |
DefaultAzureCredential: failed to acquire a token | no identity: the VM has no managed identity, the service-principal variables are missing, or workload identity is not set up | see Azure Key Vault and Custom deployments |
ForbiddenByConnection, or a connection timeout to *.vault.azure.net | the vault's firewall does not admit the server | add a private endpoint or a service endpoint |
the local key does not unwrap the stored key | Local.Key is not the key the data key was wrapped with | put the original key back |
key provider mismatch: the data encryption key is wrapped by … but the configuration names … | the provider or key changed in the configuration | put the previous block back, then run rewrap |
the database holds encrypted values but EncryptionConfig.Mode is off | the block was removed or set to off | put it back, or run decrypt |
a database decryption is running or was interrupted | decrypt did not finish | run decrypt again, or decrypt --cancel |
the store holds encrypted data but its key record is missing | encryption_keys was lost, by a partial restore or a manual delete | restore the whole database from a backup. Management does not make a new key |
EncryptionConfig.Mode on needs the sqlite, postgres or redis store engine | the server uses the legacy JSON file store | move the server to PostgreSQL first; contact Netzilo support |
a value of <table>.<column> does not decrypt and is returned as stored (warning) | a value encrypted with another key, for example copied from another server | find where it came from; the rest of the server works |
another instance turned encryption on; this instance runs with EncryptionConfig.Mode off and stops writing secrets | several instances with different configurations | give every instance the same EncryptionConfig and restart it |
renewing the vault credentials: vault token renew: … (warning) | the Vault token cannot be renewed, or is close to its maximum lifetime | switch to a periodic token or AppRole before it expires |
decrypt: <n> instance heartbeat(s) from the last 2m0s; waiting for them to age out | an instance stopped less than two minutes ago, or still runs | wait; if it still runs, stop it, or use --force |
decrypt: instance … still uses the key: stop it before the key is retired | an instance kept running | stop every management instance and run decrypt again |
When you contact Netzilo support, include the database encryption lines of the management
log and the output of encryption status. Never send management.json, tokens, keys or a
database dump.
Security notes
- Audit the key store. Each management start unwraps the data key once. Turn on the key
store's audit log (a Vault audit device, AWS CloudTrail for KMS
Decrypt, Key Vault diagnostic logs) and alert on unwraps that do not match a management start. - Least privilege. Management needs only encrypt and decrypt (Vault, KMS) or wrap and unwrap (Key Vault) on one key. Nobody else needs those rights for daily operation.
management.jsonis a secret, more so with a Vault token or a local key in it. Prefer identities without stored secrets: instance roles, managed identities, workload identity.- Copies made earlier stay readable. Turning encryption on protects copies made from then on. Dumps and backups made before stay in plain text; expire them as your retention policy allows, and rotate any credential that may have been in one that was exposed.
Related Documentation
- Backup and restore - back up before you turn it on, and restore with the key store
- Upgrade and add components - upgrade management before you turn it on
- Server reference - the compose folder,
management.jsonand the secrets a server holds - Hosting on AWS - the AWS Marketplace image and its instance role
- Hosting on Azure - the Azure Marketplace image and its managed identity

