Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions inventories/sign_nodes/group_vars/all.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
# ALBS master the sign nodes talk to. Set to the web-server host address.
albs_address: "localhost"

# Multi-host deploy is remote; each host is reached over SSH via its
# per-host ansible_host (see host_vars/).
use_local_connection: false
use_already_cloned_repos: false
ansible_interpreter_path: auto

# --- Shared identity + GPG (consumed by sign_common) ------------------------
service_user: "albs-signer"
service_group: "albs-signer"

# Auto-generate a dev key on hosts that declare no key. Set to false to require
# externally provisioned keys (see per-host pgp_keys / gpg_import_keys).
sign_generate_gpg_key: true
gpg_default_email: "test@albs.local"
gpg_default_password: "1234567890"

# --- Environment: dev vs prod -----------------------------------------------
# Dev unlocks signing keys with a static passphrase (gpg_default_password) and,
# for sign-file, seeds a test DB user. PROD MUST set this to false: the static
# dev passphrase env vars are then NOT written, and key passphrases come from
# Bitwarden instead (enable the block below).
sign_dev_mode: true

# --- Bitwarden (prod passphrases for BOTH sign-node and sign-file) ----------
# When enabled, each service fetches its GPG key passphrases from Bitwarden:
# create one login item per key id (item NAME == key id, PASSWORD == passphrase).
bitwarden_enabled: false
# Bitwarden server URL (vault.bitwarden.com or a self-hosted instance). The
# sign_common role installs the bw CLI and runs `bw config server` with this.
bitwarden_url: ""
bitwarden_username: ""
# Master vault password. Supply via Ansible Vault (do NOT commit plaintext);
# pass with --vault-password-file, or override with -e. The bitwarden_cli role
# writes it to bitwarden_password_file on each host (0600, owned by service_user).
bitwarden_master_password: ""
# Managed path the role writes the master password to; bw-ensure-session and
# the service configs read from here. Override only if you provision the file
# out-of-band (leave bitwarden_master_password empty in that case).
bitwarden_password_file: "/home/{{ service_user }}/.config/.bw-master"
# Inline password alternative — single-worker sign-file only, less safe. Leave
# empty when using bitwarden_master_password + bitwarden_password_file.
bitwarden_password: ""
# Optional: restrict the lookup to a single collection UUID.
bitwarden_collection_id: ""

# --- albs-sign-node settings ------------------------------------------------
albs_jwt_token: ""
albs_jwt_secret: "secret"

pulp_user: "admin"
pulp_password: "password"

# immudb (optional) — leave unset to disable notarization on the sign node.
# immudb_address: ""
# immudb_database: ""
# immudb_username: ""
# immudb_password: ""
...
14 changes: 14 additions & 0 deletions inventories/sign_nodes/host_vars/sign01.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
ansible_host: 10.0.0.11

# This host's signing key(s), as 16-char key ids. Authoritative when set —
# both albs-sign-node and albs-sign-file on this host sign with these.
pgp_keys:
- "7C3955C2A345DA89"

# Optional: private key files on the control node to import into this host's
# keyring before signing. Omit to rely on a key already present, or on
# auto-generation (sign_generate_gpg_key).
# gpg_import_keys:
# - files/keys/sign01-private.asc
...
10 changes: 10 additions & 0 deletions inventories/sign_nodes/host_vars/sign02.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
ansible_host: 10.0.0.12

# A DIFFERENT key from sign01 — each host has its own signing identity.
pgp_keys:
- "A1B2C3D4E5F60789"

# gpg_import_keys:
# - files/keys/sign02-private.asc
...
9 changes: 9 additions & 0 deletions inventories/sign_nodes/hosts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
all:
children:
sign_nodes:
hosts:
# Each host runs BOTH albs-sign-node and albs-sign-file, sharing that
# host's GnuPG keyring. Per-host settings (ansible_host, pgp_keys,
# gpg_import_keys) live in host_vars/<name>.yml.
sign01:
sign02:
19 changes: 19 additions & 0 deletions playbooks/albs_sign_hosts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
- name: Deploy ALBS sign hosts (sign-node + sign-file, co-located)
hosts: sign_nodes
roles:
# Shared prerequisites: service user, base packages, and the host's
# GnuPG keyring resolved into the `gpg_keys` fact. Must run first — both
# service roles below consume that fact.
- sign_common
# Bitwarden CLI (prod passphrase source), configured for the service user.
- role: bitwarden_cli
when: bitwarden_enabled | default(false) | bool
vars:
bitwarden_cli_config_user: "{{ service_user }}"
# albs-sign-node: signs build artifacts pulled from the ALBS master.
- separate_sign_node
# albs-sign-file: standalone HTTP signing API (port 8000).
- separate_sign_file
connection: "{{ 'local' if use_local_connection else 'ssh' }}"
...
5 changes: 5 additions & 0 deletions playbooks/albs_with_separate_sign_node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
- name: Deploy the separate sign node of ALBS
hosts: sign_node_vm
roles:
- sign_common
- role: bitwarden_cli
when: bitwarden_enabled | default(false) | bool
vars:
bitwarden_cli_config_user: "{{ service_user }}"
- separate_sign_node
- { role: ezamriy.fail2ban, fail2ban_ignoreip: '127.0.0.1/8 192.168.0.0/24' }
tags:
Expand Down
37 changes: 37 additions & 0 deletions roles/bitwarden_cli/defaults/main/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
# Reusable role: install the Bitwarden CLI system-wide and point it at a
# server. Include it from any service that fetches secrets from Bitwarden.
#
# Consumers typically include it conditionally and set the config user, e.g.:
# - role: bitwarden_cli
# when: bitwarden_enabled | bool
# vars:
# bitwarden_cli_config_user: "{{ service_user }}"

bitwarden_cli_version: "2026.4.1"
bitwarden_cli_url: "https://github.com/bitwarden/clients/releases/download/cli-v{{ bitwarden_cli_version }}/bw-linux-{{ bitwarden_cli_version }}.zip"

# Install dir must be on PATH for every user on the host.
bitwarden_cli_install_dir: "/usr/local/bin"

# Bitwarden server URL (vault.bitwarden.com or a self-hosted instance).
bitwarden_url: ""

# User under which `bw config server` runs (writes that user's CLI config).
# Each consuming service overrides this with its own service account.
bitwarden_cli_config_user: "root"

# Master vault password. Supply via Ansible Vault — never commit plaintext.
# When set, the role writes it to bitwarden_password_file (0600, owned by the
# config user) so bw-ensure-session and the service configs can read it. Leave
# empty to manage the file out-of-band (the role then only reads its path).
bitwarden_master_password: ""

# Path the role writes the master password to, and the path consumers
# (sign-node config, sign-file config, bw-ensure-session) read from. Keyed on
# service_user so it resolves IDENTICALLY in every role: bitwarden_cli_config_user
# is set only as a role-scoped var on this role, so consumers (separate_sign_*)
# would otherwise fall back to its "root" default and read a different path than
# the one written here. Falls back to bitwarden_cli_config_user for generic use.
bitwarden_password_file: "/home/{{ service_user | default(bitwarden_cli_config_user) }}/.config/.bw-master"
...
23 changes: 23 additions & 0 deletions roles/bitwarden_cli/files/bw-ensure-session.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
#
# Ensure a usable Bitwarden session and print the session key on stdout.
# Managed by Ansible (bitwarden_cli role). Reusable by any service that needs
# a shared BW_SESSION (e.g. multi-worker servers where each worker must reuse
# one session instead of racing `bw login`/`bw unlock`).
#
# Usage: bw-ensure-session <username> <password_file>
#
# Assumes `bw config server` has already been run for the invoking user
# (the bitwarden_cli role does this).
set -euo pipefail

username="${1:?username required}"
pass_file="${2:?password file path required}"

# `bw login --check` exits 0 when logged in, 1 otherwise.
if ! bw login --check >/dev/null 2>&1; then
bw login "${username}" --raw --passwordfile "${pass_file}" >/dev/null
fi

# Print the session key so the caller can `export BW_SESSION="$(...)"`.
bw unlock --raw --passwordfile "${pass_file}"
86 changes: 86 additions & 0 deletions roles/bitwarden_cli/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
- name: Require a Bitwarden server URL
ansible.builtin.assert:
that:
- bitwarden_url | length > 0
fail_msg: "bitwarden_cli role included but bitwarden_url is not set."

- name: Install unzip (needed to unpack the CLI archive)
ansible.builtin.dnf:
name: unzip
state: present
become: yes

- name: Download and unpack the Bitwarden CLI onto PATH
ansible.builtin.unarchive:
src: "{{ bitwarden_cli_url }}"
dest: "{{ bitwarden_cli_install_dir }}"
remote_src: yes
creates: "{{ bitwarden_cli_install_dir }}/bw"
become: yes

- name: Ensure the bw binary is executable for all users
ansible.builtin.file:
path: "{{ bitwarden_cli_install_dir }}/bw"
owner: root
group: root
mode: "0755"
become: yes

- name: Install the bw-ensure-session helper onto PATH
ansible.builtin.copy:
src: bw-ensure-session.sh
dest: "{{ bitwarden_cli_install_dir }}/bw-ensure-session"
owner: root
group: root
mode: "0755"
become: yes

# `bw config server` (no url) prints the currently configured server. Read it
# first: the CLI refuses "config server" while logged in ("Logout required
# before server config update"), so on a re-run we must skip the set when the
# server is already correct.
- name: Read the currently configured Bitwarden server
become: yes
become_user: "{{ bitwarden_cli_config_user }}"
ansible.builtin.command: "{{ bitwarden_cli_install_dir }}/bw config server"
register: bw_current_server
changed_when: false
failed_when: false

- name: Point the Bitwarden CLI at the server
become: yes
become_user: "{{ bitwarden_cli_config_user }}"
ansible.builtin.command: "{{ bitwarden_cli_install_dir }}/bw config server {{ bitwarden_url }}"
register: bw_config_server
changed_when: "'Saved' in (bw_config_server.stdout | default(''))"
when: bitwarden_url not in (bw_current_server.stdout | default(''))

# Materialize the master password so `bw login/unlock --passwordfile` (and the
# service configs) can read it. Skipped when bitwarden_master_password is empty
# — then the file is expected to be provisioned out-of-band.
#
# This role runs before the service roles that create the config dir, and copy
# does not create parents, so ensure the directory exists first. mode matches
# the service roles' 0750 to stay idempotent across runs.
- name: Ensure the directory for the Bitwarden password file exists
become: yes
ansible.builtin.file:
path: "{{ bitwarden_password_file | dirname }}"
state: directory
owner: "{{ bitwarden_cli_config_user }}"
group: "{{ bitwarden_cli_config_user }}"
mode: "0750"
when: bitwarden_master_password | length > 0

- name: Write the Bitwarden master password file
become: yes
ansible.builtin.copy:
content: "{{ bitwarden_master_password }}"
dest: "{{ bitwarden_password_file }}"
owner: "{{ bitwarden_cli_config_user }}"
group: "{{ bitwarden_cli_config_user }}"
mode: "0600"
no_log: yes
when: bitwarden_master_password | length > 0
...
58 changes: 58 additions & 0 deletions roles/separate_sign_file/defaults/main/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
# albs-sign-file: standalone FastAPI signing API (uvicorn, port 8000).
# Runs as the shared sign service user and reads the host keyring resolved by
# the sign_common role (the `gpg_keys` fact). Its own isolated venv is built
# with the system Python 3.9 — never shared with albs-sign-node.

sign_file_repo: "https://github.com/AlmaLinux/albs-sign-file.git"
sign_file_repo_version: "main"

sign_file_working_directory: "/home/{{ service_user }}/albs-sign-file"
sign_file_venv_directory: "{{ sign_file_working_directory }}/venv"

sign_file_config_dir: "/home/{{ service_user }}/.config"
sign_file_config_file: "{{ sign_file_config_dir }}/sign_file.yaml"

# Multi-worker runs need ONE shared Bitwarden session (each worker must not
# race `bw login`/`bw unlock`). When true, ExecStart establishes the session
# via bw-ensure-session and exports BW_SESSION before exec'ing uvicorn.
sign_file_bw_session_required: "{{ (bitwarden_enabled | default(false) | bool) and (sign_file_workers | int > 1) }}"

# Database backend: "sqlite" (default; keeps a standalone host dependency-free)
# or "postgresql" (point at an existing DB + role — this role does NOT create
# the Postgres database or user, only the schema via migrations).
sign_file_db_backend: "sqlite"

# SQLite settings
sign_file_db_path: "/home/{{ service_user }}/sign-file.sqlite3"

# PostgreSQL settings (used when sign_file_db_backend == "postgresql")
sign_file_pg_host: "localhost"
sign_file_pg_port: 5432
sign_file_pg_name: "signfile"
sign_file_pg_user: "signfile"
sign_file_pg_password: "signfile"

# Effective DB URL, derived from the backend. Override directly if you need a
# URL shape not covered here.
sign_file_db_url: >-
{{ ('sqlite:///' ~ sign_file_db_path)
if sign_file_db_backend == 'sqlite'
else ('postgresql://' ~ sign_file_pg_user ~ ':' ~ sign_file_pg_password
~ '@' ~ sign_file_pg_host ~ ':' ~ (sign_file_pg_port | string)
~ '/' ~ sign_file_pg_name) }}

sign_file_bind_host: "0.0.0.0"
sign_file_port: 8000
sign_file_workers: 4

sign_file_root_url: "/sign-file"
sign_file_jwt_secret: "access-secret"
sign_file_gpg_binary: "/usr/bin/gpg2"
sign_file_keyring: "/home/{{ service_user }}/.gnupg/pubring.kbx"

# Passphrase handling is driven by the shared `sign_dev_mode` and Bitwarden
# vars (see inventory group_vars). In dev, the static dev passphrase env vars
# are written to the unit; in prod they are omitted and Bitwarden (rendered
# into config.yaml) supplies passphrases.
...
Loading
Loading