Secret Managers¶
The scanner can retrieve connection credentials from a secret manager instead of storing them in plain text in connection files.
This page describes the supported secret managers, how to configure them, and the permissions required on the secret manager side.
Warning
Values such as <APP_ID> or <VAULT_HOST> are placeholders. Never include real passwords, tokens, or keys in shared configuration files. Instead, pass sensitive values through environment variables (for example, ${?MY_VAR}).
How It Works¶
A secret manager configuration has two parts:
- Scanner-level configuration: One secret manager for the whole scanner, declared in the scanner's
application.conf. - Connection-level configuration: A reference in each connection file that specifies the secret to read (
secret_manager.key) and the fields to substitute (${secret_manager.<field>}).
Scanner-Level Configuration¶
Configure a single secret manager for the scanner in application.conf:
secret-manager {
provider = "<PROVIDER>"
configuration { # provider settings; flat keys, string values
...
}
proxy { # optional
...
}
key = "scanner" # optional: the secret used for substitutions in application.conf itself (default "scanner")
}
One Secret Manager per Scanner
Only one secret manager can be configured per scanner.
- If no
secret-managerblock is configured, no secret manager is used. The log showssecret_manager_not_in_use. - Keep the values in
configurationas quoted strings, including numbers. For example,timeout-seconds = "30". - The scanner creates the provider at startup. A configuration error (such as a missing key, an unreadable keystore, or an
http://URL) stops the scanner with a message that identifies the setting.
Connection-Level Configuration¶
Configure each connection to use a secret from the secret manager:
name = "postgres-prod"
code = "postgres-prod"
connector_id = "postgresql"
secret_manager {
enabled = true
key = "zeenea/postgres-prod" # the secret to read; its format depends on the provider
}
connection {
url = "jdbc:postgresql://db.example:5432/app"
username = ${secret_manager.username} # a field of the secret
password = ${secret_manager.password}
# Optional field with a default value: kept when the secret has no "schema" field
schema = "public"
schema = ${?secret_manager.schema}
}
secret_manager.enableddefaults tofalse.- When
keyis missing, the scanner uses the connectioncodeas the key and logs the warningsecret_manager_no_identifier_key_found. - A secret is a set of named fields. Each provider section describes the fields available in a secret (for example,
usernameandpasswordfor a JSON secret). - Use
${secret_manager.<field>}for a required value, and${?secret_manager.<field>}for an optional one (standard HOCON syntax). - When the secret or field cannot be found, the scanner logs
secret_manager_missing_secretorsecret_manager_missing_key_value. A required substitution cannot be resolved (Could not resolve substitution to a value), so the connection does not load. - The scanner calls the secret manager for each substitution and caches nothing. As a result, a rotated password is picked up the next time the connection configuration is loaded.
Providers¶
The scanner supports multiple secret manager providers.
Choosing a Provider¶
Use the following table to compare the supported providers:
| Provider | Secret Store | Scanner Authentication | Network | Typical Use |
|---|---|---|---|---|
file | Local HOCON file | File permissions | None | Testing or when an external tool writes the file |
aws-secrets-manager | AWS Secrets Manager | Access key, AWS profile, or the default AWS credential chain (for example, instance role) | AWS API | Scanner running on AWS |
hashicorp-vault | HashiCorp Vault or OpenBao (KV v1/v2) | Token, AppRole, Kubernetes, or TLS certificate | HTTPS to Vault | HashiCorp Vault or OpenBao users |
gcp-secret-manager | Google Cloud Secret Manager | Service account key or the metadata server (GCE, GKE, Cloud Run) | HTTPS to Google Cloud | Scanner running on GCP |
Recommended Authentication Methods
Prefer an authentication method that does not store long-lived secrets on the scanner host. Examples include the GCP metadata server, AWS instance roles, and Vault Kubernetes authentication.
Common Settings for HTTP Providers¶
The following settings apply to hashicorp-vault and gcp-secret-manager.
Timeouts and Retries¶
| Setting | Default | Description |
|---|---|---|
timeout-seconds | "30" | Connection and response timeout for each request. |
max-retries | "2" | Number of retries after a 5xx response or a network error, including timeouts. Retries use exponential backoff (200 ms, 400 ms, 800 ms, and so on). |
The scanner does not retry 4xx responses or TLS errors because they indicate a configuration or permission problem.
TLS¶
- Only
https://URLs are accepted. The exception ishashicorp-vault, which also acceptshttp://URLs onlocalhost,127.x.x.x, or[::1]for local development and logs the warningsecret_manager_insecure_url. - The server certificate and host name are always verified.
- By default, the JVM's trusted certificates (
cacerts) are used. For a private CA or a TLS-inspecting proxy, provide a truststore:
| Setting | Default | Description |
|---|---|---|
truststore-path | — | Path to the truststore that contains the CA certificates to trust. |
truststore-password | Empty | Password for the truststore file. |
truststore-type | PKCS12 | Type of the truststore file. Possible values are PKCS12 or JKS. |
Where a client certificate is used (for example, Vault cert authentication), it is read from keystore-path, keystore-password, and keystore-type (default PKCS12). The keystore must contain both the certificate and its private key.
Create a keystore from PEM files as follows:
openssl pkcs12 -export -in client.crt -inkey client.key -certfile ca.crt \
-name client -out client.p12 # prompts for the keystore password
chmod 600 client.p12 # readable by the scanner's service user only
Proxy¶
secret-manager {
...
proxy {
hostname = "proxy.example"
port = "3128"
scheme = "http" # default "http"
username = "<PROXY_USER>" # optional
password = ${?SCANNER_PROXY_PASSWORD} # optional
}
}
hostnameandportare required. If either is missing, the warning<provider>_proxy_erroris logged and no proxy is used.- Basic proxy authentication is supported, including for HTTPS tunnels.
aws-secrets-manageruses the same block but requires all five settings. For more information, see AWS Secrets Manager.- Providers that do not make network calls, such as
file, ignore the proxy configuration.
Note
The proxy path is not covered by automated tests. Validate the configuration in your environment before deploying to production.
Local File¶
provider = "file"
Reads secrets from a local HOCON file.
Configuration¶
secret-manager {
provider = "file"
configuration {
file-path = "/etc/zeenea/secrets.conf"
}
}
Key and Fields¶
In the secret file, the first path segment is the secret key, and the remaining path segments are the field name. Values must be strings. Substitutions are resolved, so ${?ENV_VAR} works.
postgres-prod {
username = "USER_NAME"
password = ${?POSTGRES_PASSWORD}
}
- Key:
postgres-prod. - Fields:
username,password.
Setup on the Host Side¶
Protect the file with chmod 600 and ensure it is owned by the scanner's service user.
AWS Secrets Manager¶
provider = "aws-secrets-manager"
Reads secrets from AWS Secrets Manager through the AWS SDK.
Configuration¶
secret-manager {
provider = "aws-secrets-manager"
configuration {
region = "eu-west-3" # optional: default AWS region chain
profile = "zeenea" # optional: named profile from ~/.aws
access_key_id = ${?AWS_ACCESS_KEY_ID} # optional, together with secret_access_key
secret_access_key = ${?AWS_SECRET_ACCESS_KEY}
}
}
- Unlike the other providers, this provider uses underscores in setting names, such as
access_key_id. - The proxy requires
hostname,port,scheme,username, andpassword. If any setting is missing, the scanner does not use the proxy.
Authentication¶
The scanner selects credentials in this order: access_key_id and secret_access_key, then profile, then the default AWS credentials chain (environment variables, instance role, ECS or EKS role, and other sources).
On AWS, we recommend using an instance role or a pod role.
Key and Fields¶
- Key: The secret name or ARN.
- Secret value: A JSON object whose values are all strings, for example,
{"username": "USER_NAME", "password": "..."}. Other value types cause parsing to fail withaws_secret_manager_parse_error. - Fields: The keys of the JSON object.
Setup on the AWS Side¶
Grant the IAM permission secretsmanager:GetSecretValue on the secrets. If the secrets are encrypted with a customer-managed KMS key, also grant kms:Decrypt.
HashiCorp Vault and OpenBao¶
provider = "hashicorp-vault"
Reads secrets from a KV v1 or KV v2 secrets engine.
The same provider works with OpenBao. Use the OpenBao URL and the same settings. Compatibility has been verified with OpenBao 2.7.0 using token and AppRole authentication with KV v2. However, OpenBao does not guarantee API compatibility with Vault.
Configuration¶
secret-manager {
provider = "hashicorp-vault"
configuration {
url = "https://<VAULT_HOST>:8200" # required; http only on localhost
namespace = "<NAMESPACE>" # optional (Vault Enterprise / HCP / OpenBao namespaces)
mount = "secret" # default: KV mount path
kv-version = "2" # default; "1" for KV v1
auth-method = "approle" # required: token | approle | kubernetes | cert
auth-mount = "approle" # optional: defaults to the method name
# ...authentication settings, see below
truststore-path = "/etc/zeenea/vault-ca.p12" # optional
timeout-seconds = "30"
max-retries = "2"
}
proxy { ... } # optional
}
Authentication¶
auth-method | Settings | Description |
|---|---|---|
token | token (required) | A static token, for example token = ${?VAULT_TOKEN}. It is not renewed, so use a periodic token or prefer another method. |
approle | role-id and secret-id (required) | Logs in to auth/<auth-mount>/login. Pass secret-id through an environment variable. |
kubernetes | role (required), jwt-path (default /var/run/secrets/kubernetes.io/serviceaccount/token) | For a scanner running in Kubernetes. The token file is read again at each login, allowing projected tokens to rotate. |
cert | keystore-path (required), keystore-password, keystore-type, role (optional) | TLS certificate authentication. The client certificate is presented during the TLS handshake. |
- Login token: With
approle,kubernetesandcertauthentication, the scanner reuses the token until 90% of its lease has elapsed and then performs a new login to obtain a new token. After a 403 response, the scanner logs in again and retries the request once.
Key and Fields¶
- Key: The secret path within the mount, for example,
zeenea/postgres-prod. The scanner refuses a key that is empty, starts with/, or has an empty or..segment. - Fields: The top-level keys of the secret. Numbers and booleans are converted to text. Nested objects and lists are ignored with
vault_ignored_valueswarning.
Behavior¶
- Reading: KV v2 reads
GET /v1/<mount>/data/<key>(latest version). KV v1 readsGET /v1/<mount>/<key>. - Missing secret: A
404response indicates that the secret does not exist.
Setup on the Vault or OpenBao Side¶
The following example uses KV v2 mounted at secret/.
# Policy "zeenea-read": read-only access to the scanner's secrets
path "secret/data/zeenea/*" {
capabilities = ["read"]
}
vault policy write zeenea-read zeenea-read.hcl # OpenBao: bao policy write ...
vault write auth/approle/role/zeenea token_policies=zeenea-read token_ttl=1h
vault read auth/approle/role/zeenea/role-id # -> role-id
vault write -f auth/approle/role/zeenea/secret-id # -> secret-id
vault kv put secret/zeenea/postgres-prod username=USER_NAME password=...
For KV v1, the policy path does not include the data/ segment (for example, path "secret/zeenea/*").
Google Cloud Secret Manager¶
provider = "gcp-secret-manager"
Reads a secret version from Google Cloud Secret Manager through its REST API.
Configuration¶
secret-manager {
provider = "gcp-secret-manager"
configuration {
project = "<PROJECT_ID>" # required unless keys are full resource names
auth-method = "application-default" # default | service-account-key | metadata
credentials-path = "/etc/zeenea/gcp-sa.json" # service-account-key (required); application-default (optional)
truststore-path = "..." # optional, e.g. for a TLS-inspecting proxy
timeout-seconds = "30"
max-retries = "2"
}
proxy { ... } # optional
}
Authentication¶
auth-method | How it works |
|---|---|
application-default (default) | Uses credentials-path if specified, otherwise the GOOGLE_APPLICATION_CREDENTIALS environment variable, and finally the metadata server. This follows Google's Application Default Credentials order, except that the gcloud user credentials file is not supported. |
service-account-key | Uses a service account JSON key file specified by credentials-path. |
metadata | Uses the metadata server of the GCE VM, GKE pod (Workload Identity), or Cloud Run service. This is the recommended authentication method on GCP because no key file is required. |
Only service_account key files are supported. authorized_user (gcloud user credentials), external_account (Workload Identity Federation), and impersonation files are rejected at startup with a clear error message.
Key and Fields¶
| Key | Reads |
|---|---|
zeenea-postgres | The latest version, in project. |
zeenea-postgres/versions/3 | Version 3 (a version alias also works). |
projects/<PROJECT>/secrets/zeenea-postgres/versions/latest | A full resource name, in any project. |
Secret IDs and versions can contain only letters, digits, hyphens (-), and underscores (_).
- If a secret value is a JSON object, its top-level keys are returned as fields. Numbers and booleans are converted to text, and nested values are ignored.
- Any other value (for example, a plain password) is returned as a single field named value.
- The payload checksum (CRC32C) is verified.
Setup on the GCP Side¶
gcloud secrets create zeenea-postgres --data-file=secret.json # {"username": "...", "password": "..."}
gcloud secrets add-iam-policy-binding zeenea-postgres \
--member="serviceAccount:<SCANNER_SA>@<PROJECT>.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
To follow the principle of least privilege, grant the role on each secret rather than on the entire project.
Security Recommendations¶
-
Least privilege: Give the scanner's identity read-only access to the secrets it needs, and nothing else. For example, use a Vault policy on a single path prefix or per-secret IAM permissions on GCP.
-
Avoid long-lived secrets on disk when possible:
- AWS: Use an instance role or pod role.
- GCP: Use the metadata server.
- Vault: Use Kubernetes authentication.
-
When a bootstrap secret is unavoidable (for example, an AppRole
secret-idor a keystore password):- Pass it through an environment variable (
${?VAR}) rather than storing it as plain text inapplication.conf. - Restrict the key and keystore files to the scanner's service user using
chmod 600. - Rotate secrets regularly.
- Pass it through an environment variable (
-
Dedicated service account: Run the scanner under its own OS user, which the file permissions rely on.
-
TLS: Keep the default certificate verification enabled. Add your CA to a truststore rather than disabling verification. The scanner does not provide an option to disable these checks.
-
Audit: Monitor the secret manager's access logs for the scanner's identity (for example, Vault audit logs, AWS CloudTrail, or Google Cloud Audit Logs).
-
Logs: The scanner never logs secret values, tokens, or private keys. It logs keys (secret names or queries), status codes, and the error messages of the provider.
Troubleshooting¶
Log Prefixes¶
Log events start with a provider-specific prefix:
| Provider | Prefix |
|---|---|
aws-secrets-manager | aws_secret_manager_ |
hashicorp-vault | vault_ |
gcp-secret-manager | gcp_secret_manager_ |
Common Issues¶
| Symptom or Log | Likely Cause | Check or Fix |
|---|---|---|
The scanner does not start: Unknown secret manager provider | A typo in provider | Use one of the supported provider values listed in Choosing a Provider. |
The scanner does not start: Key not found: '<setting>' or Invalid '<setting>' | A required setting is missing or invalid | The error message identifies the setting. See the corresponding provider section. |
Invalid 'url': ... must be an https URL | An http:// URL | Use https:// URL. Only Vault accepts http://localhost for development. |
Unable to load the ... keystore/truststore | Incorrect path, password, or type | Test the file with keytool -list -keystore <file> -storetype PKCS12. The file must be readable by the scanner user. |
secret_manager_missing_secret | The secret was not found, or access was denied | Check the provider's preceding *_read_error log event. A 404 error indicates an incorrect key or mount, while a 403 error indicates insufficient permissions. |
secret_manager_missing_key_value | The secret exists but does not contain the requested field | Check the field names defined in the corresponding provider section (for example, value for a non-JSON GCP secret). |
vault_login_error, gcp_secret_manager_token_error | Authentication failed | Check the credentials and the role. The error message of the provider is included. |
*_read_error with an SSL/TLS message | The server certificate is not trusted, or the host name does not match | Configure truststore-path with the appropriate CA certificate and use the host name specified in the certificate. |
*_ignored_values | The secret contains nested JSON values | Only top-level text, number, and boolean values are used. Flatten the secret structure. |
*_proxy_error | An incomplete proxy block | See Proxy. |
| Timeouts | A network or proxy issue | Test connectivity with curl from the scanner host using the same proxy configuration. Increase timeout-seconds if needed. |