AlmaLinux / AlmaLinux/build-system

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

Open
#548 1 comment 0 reactions 1 assignee Claimed by @anfimovdm View on GitHub
Dominant language
No language data
Stars
32
Forks
11
PR merge metrics
No merged PRs in 30d

Description

### Background

[#280](https://github.com/AlmaLinux/build-system/issues/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):

```yaml
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):

```yaml
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 `//` 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](https://github.com/AlmaLinux/build-system/issues/280) — Bitwarden (and other vaults) integration in sign service (closed; this is the unfinished half)
- [#542](https://github.com/AlmaLinux/build-system/issues/542) — intermittent RPM sign failures, gpg-agent passphrase cache expiring

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.