From 24fbced3d05c725c34a98a79566a973b9f30e95b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 03:53:17 +0000 Subject: [PATCH 1/4] Run Ansible from CI with a dedicated ci user Add a users role that creates the ci automation user (locked password, repo-managed authorized key, NOPASSWD sudo via /etc/sudoers.d/ci) and the letsbuilda service user the minecraft role depends on but nothing created. Deploy via a new ansible-deploy workflow on pushes to main touching ansible/**, gated by the ansible GitHub Environment, connecting as ci with a pinned host key. PRs get an ansible-lint + syntax check job (the existing .ansible-lint config was never actually run in CI). Drop the vault_password_file setting pointing at a nonexistent file and document local runs, CI deploys, and the bootstrap procedure in the README. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0131bYS1tMnLNHUJYQy8HwEW --- .github/workflows/ansible-deploy.yaml | 44 ++++++++++++++++++++++++ .github/workflows/lint.yaml | 26 ++++++++++++++ .gitignore | 1 + README.md | 43 +++++++++++++++++++++++ ansible/ansible.cfg | 1 - ansible/known_hosts | 3 ++ ansible/playbook.yaml | 3 ++ ansible/roles/users/files/ci-sudoers | 1 + ansible/roles/users/files/ci_ed25519.pub | 1 + ansible/roles/users/tasks/main.yaml | 32 +++++++++++++++++ 10 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ansible-deploy.yaml create mode 100644 .gitignore create mode 100644 ansible/known_hosts create mode 100644 ansible/roles/users/files/ci-sudoers create mode 100644 ansible/roles/users/files/ci_ed25519.pub create mode 100644 ansible/roles/users/tasks/main.yaml diff --git a/.github/workflows/ansible-deploy.yaml b/.github/workflows/ansible-deploy.yaml new file mode 100644 index 0000000..bf18540 --- /dev/null +++ b/.github/workflows/ansible-deploy.yaml @@ -0,0 +1,44 @@ +--- +name: Deploy Ansible configuration +on: + push: + branches: [main] + paths: [ansible/**] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ansible-deploy + cancel-in-progress: false +jobs: + ansible-playbook: + environment: ansible + name: Run the playbook against all hosts + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + cache-dependency-glob: uv.lock + activate-environment: true + - name: Install dependencies + run: uv sync --frozen --only-group ansible + - name: Configure SSH + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: |- + install -d -m 0700 "$HOME/.ssh" + printf '%s\n' "$SSH_PRIVATE_KEY" > "$HOME/.ssh/id_ed25519" + chmod 0600 "$HOME/.ssh/id_ed25519" + cat ansible/known_hosts >> "$HOME/.ssh/known_hosts" + - name: Run the playbook + working-directory: ansible + env: + ANSIBLE_REMOTE_USER: ci + ANSIBLE_BECOME_ASK_PASS: 'false' + run: uv run ansible-playbook --diff playbook.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d8482d8..6241b18 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -27,3 +27,29 @@ jobs: run: uv sync - name: Run prek run: uv run prek run --all-files + ansible-lint: + name: Ansible lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version-file: pyproject.toml + - name: Sync dependencies + run: uv sync --frozen --only-group ansible + - name: Run ansible-lint + working-directory: ansible + run: uv run ansible-lint + - name: Run playbook syntax check + working-directory: ansible + env: + ANSIBLE_BECOME_ASK_PASS: 'false' + run: uv run ansible-playbook --syntax-check playbook.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..447be10 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ansible/vault_password diff --git a/README.md b/README.md index eafbb33..d4d5c09 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ # infrastructure Our infrastructure + +## Ansible + +Server configuration lives in `ansible/`. All commands below run from that directory. + +### Running locally + +``` +uv sync --group ansible +uv run ansible-playbook --diff playbook.yaml +``` + +You connect as your own user and are prompted for your sudo (BECOME) password. + +### CI deploys + +Pushes to `main` that touch `ansible/**` (and manual `workflow_dispatch` runs) run the +playbook from GitHub Actions via the `ansible` GitHub Environment. CI connects as the +dedicated `ci` user (created by the `users` role) using the `SSH_PRIVATE_KEY` +environment secret, with passwordless sudo granted by `/etc/sudoers.d/ci`. The host +key is pinned in `ansible/known_hosts`, keeping strict host key checking enabled. + +### Bootstrapping / key rotation + +The `ci` user only exists after the playbook has run once, so a new host needs one +local run (as yourself, with your sudo password) before CI can deploy: + +1. Generate the CI keypair: + `ssh-keygen -t ed25519 -N '' -C 'ci@letsbuilda/infrastructure' -f ./ci_ed25519` +2. Put the public key in `ansible/roles/users/files/ci_ed25519.pub`. +3. Pin the host key: run `ssh-keyscan -t ed25519 microwave.box.letsbuilda.dev` from a + trusted network, verify the fingerprint out-of-band, and add the output to + `ansible/known_hosts`. +4. Add the private key as the `SSH_PRIVATE_KEY` secret in the `ansible` GitHub + Environment (restrict the environment's deployment branches to `main`). +5. Run the playbook locally once to create the `ci` user, then verify the CI auth + path: + `ssh -i ./ci_ed25519 -o UserKnownHostsFile=ansible/known_hosts ci@microwave.box.letsbuilda.dev 'sudo -n true && echo sudo-ok'` +6. Shred the local private key copy. + +To rotate the CI key: repeat with a new keypair, replacing the committed public key +and the `SSH_PRIVATE_KEY` secret, then run the playbook once (locally or via the old +key). If the host is reinstalled, refresh `ansible/known_hosts` the same way as step 3. diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index b6ee802..a282d83 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -1,6 +1,5 @@ [defaults] inventory = inventory/hosts.yaml -vault_password_file = vault_password [privilege_escalation] become = yes diff --git a/ansible/known_hosts b/ansible/known_hosts new file mode 100644 index 0000000..57adb23 --- /dev/null +++ b/ansible/known_hosts @@ -0,0 +1,3 @@ +# Pinned SSH host keys for managed hosts, appended to ~/.ssh/known_hosts in CI. +# Populate with `ssh-keyscan -t ed25519 microwave.box.letsbuilda.dev` from a +# trusted network, after verifying the fingerprint out-of-band. diff --git a/ansible/playbook.yaml b/ansible/playbook.yaml index 9d7f969..8573f2f 100644 --- a/ansible/playbook.yaml +++ b/ansible/playbook.yaml @@ -1,4 +1,7 @@ --- +- name: Manage users + hosts: all + roles: [users] - name: Run common setup hosts: all roles: [common] diff --git a/ansible/roles/users/files/ci-sudoers b/ansible/roles/users/files/ci-sudoers new file mode 100644 index 0000000..31ff177 --- /dev/null +++ b/ansible/roles/users/files/ci-sudoers @@ -0,0 +1 @@ +ci ALL=(ALL:ALL) NOPASSWD: ALL diff --git a/ansible/roles/users/files/ci_ed25519.pub b/ansible/roles/users/files/ci_ed25519.pub new file mode 100644 index 0000000..dcae392 --- /dev/null +++ b/ansible/roles/users/files/ci_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 REPLACE_WITH_CI_PUBLIC_KEY ci@letsbuilda/infrastructure diff --git a/ansible/roles/users/tasks/main.yaml b/ansible/roles/users/tasks/main.yaml new file mode 100644 index 0000000..77bfb17 --- /dev/null +++ b/ansible/roles/users/tasks/main.yaml @@ -0,0 +1,32 @@ +--- +- name: Create the ci automation user + ansible.builtin.user: + name: ci + shell: /bin/bash + create_home: true + password_lock: true +- name: Authorize the CI deploy key for the ci user + ansible.posix.authorized_key: + user: ci + key: "{{ lookup('file', 'ci_ed25519.pub') }}" + exclusive: true +- name: Grant the ci user passwordless sudo + ansible.builtin.copy: + src: ci-sudoers + dest: /etc/sudoers.d/ci + owner: root + group: root + mode: '0440' + validate: /usr/sbin/visudo -cf %s +- name: Create the docker group + ansible.builtin.group: + name: docker + system: true +- name: Create the letsbuilda service user + ansible.builtin.user: + name: letsbuilda + system: true + shell: /usr/sbin/nologin + create_home: true + groups: [docker] + append: true From 64aea4462985b79bcf2f05519ec7378eafce1b88 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 04:14:39 +0000 Subject: [PATCH 2/4] Move Ansible docs from the README to docs/ Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0131bYS1tMnLNHUJYQy8HwEW --- README.md | 43 ++----------------------------------------- docs/ansible.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 docs/ansible.md diff --git a/README.md b/README.md index d4d5c09..3ac4c1a 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,6 @@ # infrastructure Our infrastructure -## Ansible +## Documentation -Server configuration lives in `ansible/`. All commands below run from that directory. - -### Running locally - -``` -uv sync --group ansible -uv run ansible-playbook --diff playbook.yaml -``` - -You connect as your own user and are prompted for your sudo (BECOME) password. - -### CI deploys - -Pushes to `main` that touch `ansible/**` (and manual `workflow_dispatch` runs) run the -playbook from GitHub Actions via the `ansible` GitHub Environment. CI connects as the -dedicated `ci` user (created by the `users` role) using the `SSH_PRIVATE_KEY` -environment secret, with passwordless sudo granted by `/etc/sudoers.d/ci`. The host -key is pinned in `ansible/known_hosts`, keeping strict host key checking enabled. - -### Bootstrapping / key rotation - -The `ci` user only exists after the playbook has run once, so a new host needs one -local run (as yourself, with your sudo password) before CI can deploy: - -1. Generate the CI keypair: - `ssh-keygen -t ed25519 -N '' -C 'ci@letsbuilda/infrastructure' -f ./ci_ed25519` -2. Put the public key in `ansible/roles/users/files/ci_ed25519.pub`. -3. Pin the host key: run `ssh-keyscan -t ed25519 microwave.box.letsbuilda.dev` from a - trusted network, verify the fingerprint out-of-band, and add the output to - `ansible/known_hosts`. -4. Add the private key as the `SSH_PRIVATE_KEY` secret in the `ansible` GitHub - Environment (restrict the environment's deployment branches to `main`). -5. Run the playbook locally once to create the `ci` user, then verify the CI auth - path: - `ssh -i ./ci_ed25519 -o UserKnownHostsFile=ansible/known_hosts ci@microwave.box.letsbuilda.dev 'sudo -n true && echo sudo-ok'` -6. Shred the local private key copy. - -To rotate the CI key: repeat with a new keypair, replacing the committed public key -and the `SSH_PRIVATE_KEY` secret, then run the playbook once (locally or via the old -key). If the host is reinstalled, refresh `ansible/known_hosts` the same way as step 3. +- [Ansible](docs/ansible.md) — server configuration, CI deploys, bootstrapping diff --git a/docs/ansible.md b/docs/ansible.md new file mode 100644 index 0000000..d38b993 --- /dev/null +++ b/docs/ansible.md @@ -0,0 +1,42 @@ +# Ansible + +Server configuration lives in `ansible/`. All commands below run from that directory. + +## Running locally + +``` +uv sync --group ansible +uv run ansible-playbook --diff playbook.yaml +``` + +You connect as your own user and are prompted for your sudo (BECOME) password. + +## CI deploys + +Pushes to `main` that touch `ansible/**` (and manual `workflow_dispatch` runs) run the +playbook from GitHub Actions via the `ansible` GitHub Environment. CI connects as the +dedicated `ci` user (created by the `users` role) using the `SSH_PRIVATE_KEY` +environment secret, with passwordless sudo granted by `/etc/sudoers.d/ci`. The host +key is pinned in `ansible/known_hosts`, keeping strict host key checking enabled. + +## Bootstrapping / key rotation + +The `ci` user only exists after the playbook has run once, so a new host needs one +local run (as yourself, with your sudo password) before CI can deploy: + +1. Generate the CI keypair: + `ssh-keygen -t ed25519 -N '' -C 'ci@letsbuilda/infrastructure' -f ./ci_ed25519` +2. Put the public key in `ansible/roles/users/files/ci_ed25519.pub`. +3. Pin the host key: run `ssh-keyscan -t ed25519 microwave.box.letsbuilda.dev` from a + trusted network, verify the fingerprint out-of-band, and add the output to + `ansible/known_hosts`. +4. Add the private key as the `SSH_PRIVATE_KEY` secret in the `ansible` GitHub + Environment (restrict the environment's deployment branches to `main`). +5. Run the playbook locally once to create the `ci` user, then verify the CI auth + path: + `ssh -i ./ci_ed25519 -o UserKnownHostsFile=ansible/known_hosts ci@microwave.box.letsbuilda.dev 'sudo -n true && echo sudo-ok'` +6. Shred the local private key copy. + +To rotate the CI key: repeat with a new keypair, replacing the committed public key +and the `SSH_PRIVATE_KEY` secret, then run the playbook once (locally or via the old +key). If the host is reinstalled, refresh `ansible/known_hosts` the same way as step 3. From ed9819467d3afd5e84910abc252be44696f828a2 Mon Sep 17 00:00:00 2001 From: Bradley Reynolds Date: Sat, 29 Aug 2026 23:29:38 -0500 Subject: [PATCH 3/4] Add public key Signed-off-by: Bradley Reynolds --- ansible/roles/users/files/ci_ed25519.pub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/users/files/ci_ed25519.pub b/ansible/roles/users/files/ci_ed25519.pub index dcae392..c122ba0 100644 --- a/ansible/roles/users/files/ci_ed25519.pub +++ b/ansible/roles/users/files/ci_ed25519.pub @@ -1 +1 @@ -ssh-ed25519 REPLACE_WITH_CI_PUBLIC_KEY ci@letsbuilda/infrastructure +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL5Qm8eF6Sqdu+sOKMimDAWIJVlgmgyBQQhcOqapHmMI ci@letsbuilda/infrastructure From d9c1bbe82da55f2e0978e75e75d13e22950af506 Mon Sep 17 00:00:00 2001 From: Bradley Reynolds Date: Sat, 29 Aug 2026 23:30:00 -0500 Subject: [PATCH 4/4] Add known host Signed-off-by: Bradley Reynolds --- ansible/known_hosts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ansible/known_hosts b/ansible/known_hosts index 57adb23..404bbc3 100644 --- a/ansible/known_hosts +++ b/ansible/known_hosts @@ -1,3 +1,2 @@ -# Pinned SSH host keys for managed hosts, appended to ~/.ssh/known_hosts in CI. -# Populate with `ssh-keyscan -t ed25519 microwave.box.letsbuilda.dev` from a -# trusted network, after verifying the fingerprint out-of-band. +# microwave.box.letsbuilda.dev:22 SSH-2.0-OpenSSH_10.2p1 Ubuntu-2ubuntu3.5 +microwave.box.letsbuilda.dev ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA19eLgakX4Y69JrGaMetJlg4YmTBC7KoYvhnGiX83Iz