AlmaLinux / AlmaLinux/build-system

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

Abierto
#548 1 comentario 0 reacciones 1 asignado Reclamado por @anfimovdm Ver en GitHub
Lenguaje dominante
Sin datos de lenguaje
Estrellas
32
Forks
11
Métricas de merge de PR
Sin PR fusionados en 30 d

Descripción

### 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

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.