Skip to content

albs-sign-node, albs-sign-file: support fetching GPG key passphrases from HashiCorp Vault #548

Description

@anfimovdm

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

  • With vault_enabled: true and all keyids present in Vault, both
    services start with no interactive prompt and sign successfully.
  • A missing keyid, an unreachable/sealed Vault, or a bad token fails at
    startup with a clear error naming the offending keyid or condition —
    no silent fallback to a prompt or to a dev passphrase.
  • Enabling Bitwarden and Vault simultaneously is rejected at config
    validation time.
  • hvac is not required when the backend is disabled.
  • Unit tests with a mocked hvac client, mirroring
    tests/sign_node/utils/test_bitwarden.py and the sign-file equivalent.
  • README / config.yaml / node-config/sign_node.yml document the new
    keys and the expected Vault secret layout.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status
In review

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions