Background
#280 ("Bitwarden (and other vaults) integration in sign service") added the ability to
fetch GPG key passphrases from a Bitwarden vault, and was closed once Bitwarden
shipped. The "other vaults" part was never done, so HashiCorp Vault is not an
option for sign nodes today.
Currently a sign node operator has exactly two non-interactive choices:
Bitwarden, or development_mode with a single shared passphrase. Everything
else falls back to an interactive getpass prompt at startup, which is what
makes unattended restarts painful.
Goal
Allow both sign services to pull per-key GPG passphrases from HashiCorp Vault
at startup, as an alternative to Bitwarden.
Why this is small
The Bitwarden work already introduced the right seam. Passphrases are resolved
once at startup and handed to PGPPasswordDB as preloaded_passwords:
albs-sign-node: almalinux_sign_node.py calls
sign_node/utils/bitwarden.py::fetch_passphrases() when
config.bitwarden_enabled, and passes the result into
PGPPasswordDB(..., preloaded_passwords=...)
(sign_node/utils/pgp_utils.py).
albs-sign-file: same shape — sign/pgp/bitwarden.py::fetch_passphrases(),
consumed by sign/pgp/pgp_password_db.py, whose documented precedence is
already preloaded passwords > development password > interactive prompt.
So Vault support is a second provider returning the same
Dict[keyid, passphrase], not a rework of the signing flow.
Proposed scope
1. Provider abstraction
Rather than bolting a second if onto each entrypoint, introduce a small
secrets provider layer with bitwarden and vault implementations, both
returning Dict[str, str] keyed by GPG keyid and raising
ConfigurationError when a requested keyid is missing (Bitwarden's current
fail-fast behaviour — do not silently fall through to an interactive prompt
when a backend is explicitly enabled).
Enabling two providers at once should be a configuration error, not a
merge — signing keys should have one unambiguous source of truth.
2. albs-sign-node config (flat keys, cerberus schema in
sign_node/config.py, alongside the existing bitwarden_* keys):
vault_enabled: false
vault_addr: https://vault.example.com:8200
vault_namespace: # optional, Vault Enterprise / HCP
vault_mount: secret # KV v2 mount point
vault_path_prefix: albs/sign-keys
vault_token_file: /run/secrets/vault_token
vault_role_id: # AppRole alternative to a static token
vault_secret_id_file:
vault_ca_cert: # optional custom CA bundle
3. albs-sign-file config (nested pydantic section in sign/config.py,
mirroring the bitwarden: block in config.yaml, with SF_VAULT_* env
overrides):
vault:
enabled: false
addr: https://vault.example.com:8200
mount: secret
path_prefix: albs/sign-keys
token_file: /run/secrets/vault_token
4. Secret layout
One KV v2 secret per key at <mount>/<path_prefix>/<keyid> with a
passphrase field, e.g.:
vault kv put secret/albs/sign-keys/7C3955C2A345DA89 passphrase='...'
This keeps the Bitwarden convention (one item per keyid) while using a
field name that actually describes the value, instead of Bitwarden's
password. The field name should be configurable for operators with an
existing layout.
5. Authentication
Support a static token (from *_token_file, preferred over an inline value)
and AppRole (role_id + secret_id_file). VAULT_TOKEN / VAULT_ADDR from
the environment should be honoured as a fallback so the services work
naturally on a host that already has a Vault agent.
Token renewal is explicitly out of scope: passphrases are read once at
startup, so a short-lived token is sufficient and no long-lived session needs
to be maintained in-process.
6. Dependency
Use hvac. In albs-sign-file add it as a [vault] extra next to the
existing [bitwarden] extra in setup.py, and import it lazily so the
dependency is only required when the backend is enabled (same pattern as
sign/pgp/bitwarden.py). Mirror the lazy import in albs-sign-node.
Acceptance criteria
Out of scope
- Storing the GPG private keys themselves in Vault (only passphrases here).
- Vault Transit as a signing backend — that would replace local GPG entirely
and deserves its own issue.
- Ansible/deployment wiring in
albs-deploy — follow-up once the config
surface is settled.
Related
- #280 — Bitwarden (and other vaults) integration in sign service (closed; this is the unfinished half)
- #542 — intermittent RPM sign failures, gpg-agent passphrase cache expiring
Background
#280 ("Bitwarden (and other vaults) integration in sign service") added the ability to
fetch GPG key passphrases from a Bitwarden vault, and was closed once Bitwarden
shipped. The "other vaults" part was never done, so HashiCorp Vault is not an
option for sign nodes today.
Currently a sign node operator has exactly two non-interactive choices:
Bitwarden, or
development_modewith a single shared passphrase. Everythingelse falls back to an interactive
getpassprompt at startup, which is whatmakes unattended restarts painful.
Goal
Allow both sign services to pull per-key GPG passphrases from HashiCorp Vault
at startup, as an alternative to Bitwarden.
Why this is small
The Bitwarden work already introduced the right seam. Passphrases are resolved
once at startup and handed to
PGPPasswordDBaspreloaded_passwords:albs-sign-node:almalinux_sign_node.pycallssign_node/utils/bitwarden.py::fetch_passphrases()whenconfig.bitwarden_enabled, and passes the result intoPGPPasswordDB(..., preloaded_passwords=...)(
sign_node/utils/pgp_utils.py).albs-sign-file: same shape —sign/pgp/bitwarden.py::fetch_passphrases(),consumed by
sign/pgp/pgp_password_db.py, whose documented precedence isalready
preloaded passwords > development password > interactive prompt.So Vault support is a second provider returning the same
Dict[keyid, passphrase], not a rework of the signing flow.Proposed scope
1. Provider abstraction
Rather than bolting a second
ifonto each entrypoint, introduce a smallsecretsprovider layer withbitwardenandvaultimplementations, bothreturning
Dict[str, str]keyed by GPG keyid and raisingConfigurationErrorwhen a requested keyid is missing (Bitwarden's currentfail-fast behaviour — do not silently fall through to an interactive prompt
when a backend is explicitly enabled).
Enabling two providers at once should be a configuration error, not a
merge — signing keys should have one unambiguous source of truth.
2. albs-sign-node config (flat keys, cerberus schema in
sign_node/config.py, alongside the existingbitwarden_*keys):3. albs-sign-file config (nested pydantic section in
sign/config.py,mirroring the
bitwarden:block inconfig.yaml, withSF_VAULT_*envoverrides):
4. Secret layout
One KV v2 secret per key at
<mount>/<path_prefix>/<keyid>with apassphrasefield, e.g.:This keeps the Bitwarden convention (one item per keyid) while using a
field name that actually describes the value, instead of Bitwarden's
password. The field name should be configurable for operators with anexisting layout.
5. Authentication
Support a static token (from
*_token_file, preferred over an inline value)and AppRole (
role_id+secret_id_file).VAULT_TOKEN/VAULT_ADDRfromthe environment should be honoured as a fallback so the services work
naturally on a host that already has a Vault agent.
Token renewal is explicitly out of scope: passphrases are read once at
startup, so a short-lived token is sufficient and no long-lived session needs
to be maintained in-process.
6. Dependency
Use
hvac. Inalbs-sign-fileadd it as a[vault]extra next to theexisting
[bitwarden]extra insetup.py, and import it lazily so thedependency is only required when the backend is enabled (same pattern as
sign/pgp/bitwarden.py). Mirror the lazy import inalbs-sign-node.Acceptance criteria
vault_enabled: trueand all keyids present in Vault, bothservices start with no interactive prompt and sign successfully.
startup with a clear error naming the offending keyid or condition —
no silent fallback to a prompt or to a dev passphrase.
validation time.
hvacis not required when the backend is disabled.hvacclient, mirroringtests/sign_node/utils/test_bitwarden.pyand the sign-file equivalent.config.yaml/node-config/sign_node.ymldocument the newkeys and the expected Vault secret layout.
Out of scope
and deserves its own issue.
albs-deploy— follow-up once the configsurface is settled.
Related