Configuration
AutoHSM configuration
AutoHSM reads one YAML file, strictly validated at startup. Unknown keys, loose file permissions, plaintext URLs and ambiguous PIN sources are refused.
Commands and flags
autohsm watch [--config PATH] run the daemon (systemd entrypoint) autohsm status [--config PATH] print seal state; exit 2 if sealed autohsm wrap [--config PATH] --index N wrap a share read from stdin autohsm selftest [--config PATH] verify config, TLS pin, HSM, and shares
| Flag | Default | Notes |
|---|---|---|
--config PATH | /etc/autohsm/autohsm.yaml | Also accepted as --config=PATH |
--index N | required for wrap | Share index, 1 or higher. Bound into the wrapped envelope. |
status never opens the HSM and prints sealed=… initialized=… threshold=… shares=… progress=… version=…. selftest checks the config, the pinned TLS connection, HSM login, that every configured share unwraps for this node, and refuses a node that holds enough shares to meet the unseal threshold alone.
Configuration file rules
| Rule | Detail |
|---|---|
| Permissions | Regular file, either mode 0600 owned by the user running autohsm, or mode 0640 owned by root. The same rule applies to the PIN file and wrapped shares. |
| Size | 1 MiB maximum |
| Strict keys | Unknown keys are an error, so a mistyped security setting cannot be silently ignored |
| Single document | Multiple YAML documents are rejected |
| FIFOs and devices | Refused; symlinks are followed but the target must pass the same checks |
Configuration reference
Top level
| Key | Type | Default | Notes |
|---|---|---|---|
node_id | string | required | Operator-chosen context label bound into every wrapped share. Not hardware identity. Changing it means re-wrapping the shares. |
vault
| Key | Type | Default | Notes |
|---|---|---|---|
vault.address | string | required | Must start with https:// |
vault.ca_cert_path | path | required | CA or self-signed leaf that signs the server certificate. No fallback to system roots. |
vault.timeout | duration | 10s | Must be positive |
vault.require_tls13 | bool | true | Set false only for a server that cannot negotiate TLS 1.3 |
keys
| Key | Type | Default | Notes |
|---|---|---|---|
keys.source | string | pkcs11 | pkcs11 for production, file for development only |
keys.allow_insecure_file_source | bool | false | Must be true to use source: file |
keys.pkcs11.module_path | path | required for pkcs11 | PKCS#11 module, for example /usr/lib/softhsm/libsofthsm2.so |
keys.pkcs11.token_label | string | first token | Optional |
keys.pkcs11.key_label | string | required for pkcs11 | Label of the non-extractable AES-256 wrapping key |
keys.pkcs11.pin_file | path | not set | File holding the HSM PIN (4096 bytes max, trailing newline trimmed). Re-read for every login. |
keys.pkcs11.pin_env | string | not set | Name of an environment variable holding the PIN. Refused by watch; for interactive provisioning only. |
keys.shares | list | required | Shares this node holds; at least one |
keys.shares[].index | int | required | 1 or higher, unique |
keys.shares[].path | path | required | File holding the wrapped share |
Exactly one of pin_file or pin_env must be set; inline PINs are not supported. At startup the selected key must be AES-256, sensitive, always-sensitive, non-extractable, never-extractable and allowed to encrypt and decrypt with AES-GCM, or the daemon fails closed.
watch
| Key | Type | Default | Notes |
|---|---|---|---|
watch.interval | duration | 30s | Polling interval, 1s minimum |
watch.max_unseal_attempts | int | 5 | Consecutive rejected unseal cycles before the daemon stops trying. 1 minimum. |
alarm
| Key | Type | Default | Notes |
|---|---|---|---|
alarm.webhook_url | string | not set | Optional. https only, no credentials in the URL. Redirects are not followed; 5 second timeout. |
Example configuration
/etc/autohsm/autohsm.yaml
# Install root:autohsm with mode 0640. Owner-only 0600 is also accepted.
node_id: node-a
vault:
address: https://vault.example.com:8200
ca_cert_path: /etc/autohsm/vault-ca.pem
timeout: 10s
require_tls13: true
keys:
source: pkcs11
pkcs11:
module_path: /usr/lib/softhsm/libsofthsm2.so
token_label: autohsm
key_label: autohsm-wrap
pin_file: /etc/autohsm/pin # root:autohsm, mode 0640
# pin_env: AUTOHSM_PIN # interactive provisioning only
# Keep this list shorter than the unseal threshold.
shares:
- index: 1
path: /etc/autohsm/share-1.wrapped
watch:
interval: 30s
max_unseal_attempts: 5
alarm:
# webhook_url: https://alerts.example.com/hooks/sealedDevelopment key source
keys:
source: file
allow_insecure_file_source: true # no hardware protection
shares:
- index: 1
path: ./share-1.wrappedThe file source reads a 64 hex character AES-256 key from AUTOHSM_DEV_KEY. It exercises the same envelope and binding logic but offers no hardware protection.
Environment variables
| Variable | Used by | Notes |
|---|---|---|
| Name set in pin_env | status, wrap, selftest | HSM PIN. Removed from the process environment after reading. Not allowed for watch. |
AUTOHSM_DEV_KEY | keys.source: file | 64 hex characters (32 bytes) |
AUTOHSM_TEST_MODULE | make test-integration | Path to libsofthsm2.so for the PKCS#11 integration test |
Files and paths
| Path | Purpose |
|---|---|
/etc/autohsm/autohsm.yaml | Configuration (default --config) |
/etc/autohsm/pin | HSM PIN file |
/etc/autohsm/vault-ca.pem | Pinned CA for the server certificate |
/etc/autohsm/share-N.wrapped | Wrapped share, format autohsm-v1.<nonce>.<ciphertext||tag> |
/run/autohsm/submitted-shares | Share indexes accepted during the current unseal attempt, keyed to the server's unseal nonce, so a crashed daemon does not resubmit |
/usr/local/bin/autohsm | Binary location used by the shipped unit |
Alarm webhook
With alarm.webhook_url set, the daemon POSTs JSON with only non-secret state:
{
"event": "vault_sealed",
"node_id": "node-a",
"sealed": true,
"threshold": 3,
"shares": 5,
"progress": 0,
"timestamp": "2026-09-28T12:00:00Z"
}| event | When |
|---|---|
vault_sealed | Every poll that finds the server sealed, even if the unseal then succeeds |
vault_unreachable | Every poll that cannot reach the server |
hsm_unavailable | Once when the HSM session is lost and cannot be reopened. Reconnects back off up to 5 minutes. |
hsm_recovered | When the HSM session is back |
shares_stale | The server rejected the key material itself. Submissions stop; reminders back off up to hourly. |
autohsm_failed | Just before the daemon exits on an error |
error is present only on failure events. Delivery is best effort; pair it with an external monitor such as autohsm status from cron.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error (restarted by systemd) |
| 2 | status found the server sealed |
| 78 | Terminal failure in watch: rejected PIN, unloadable module, token without AES-GCM, missing or unsafe key, configuration error, stale shares or unsafe layout. The shipped unit does not restart on 78. |
systemd unit
deploy/autohsm.service (excerpt)
[Unit] After=network-online.target StartLimitIntervalSec=5min StartLimitBurst=5 [Service] ExecStart=/usr/local/bin/autohsm watch --config /etc/autohsm/autohsm.yaml Restart=on-failure RestartPreventExitStatus=78 RestartSec=10s User=autohsm Group=autohsm NoNewPrivileges=yes ProtectSystem=strict ReadOnlyPaths=/etc/autohsm RuntimeDirectory=autohsm RuntimeDirectoryMode=0700 UMask=0077 LimitCORE=0
For hardware HSMs, add only what the vendor module needs in a drop-in (systemctl edit autohsm): ReadWritePaths= for client state, ProcSubset=all for FIPS libraries, AF_NETLINK in RestrictAddressFamilies= for network HSM clients, and SupplementaryGroups= for a USB or PCIe token's udev group. Run selftest under the same properties with systemd-run before enabling.