Files
config-public/ansible/playbooks/bootstrap.yml
T
Lucas WintherandClaude Opus 5.5 792f7cde48 Add the host bootstrap: deploy user, key, docker access, stack root
bootstrap.yml prepares a host as an admin via -u; new-deploy-key.sh makes the
key pair and refuses to write inside the repo. CI deploys refuse the bootstrap
playbook, since the dispatch API ignores the dropdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:07:31 +02:00

125 lines
4.8 KiB
YAML

---
# Prepare a host for everything else in this repository: the deploy user, its
# key, its access to Docker, and the directory the stacks live in.
#
# The one playbook that cannot connect as the deploy user, because it is what
# creates it. Run it as an administrator who already has access:
#
# ansible-playbook playbooks/bootstrap.yml -u <admin-user> -k -K
#
# -u is what makes this work, and it works only because the deploy account is
# `remote_user` in ansible.cfg rather than ansible_user in the inventory. An
# inventory variable outranks -u; a config default does not. From the host
# itself, `-c local -K` skips SSH and the question with it.
#
# -k asks for the SSH password and -K for the sudo password; drop either if that
# account already uses a key or has passwordless sudo. SSH_KEY_PATH is not
# needed — an empty ansible_ssh_private_key_file is ignored by the ssh plugin,
# so group_vars/all.yml does not get in the way here either.
#
# Deliberately not imported by site.yml and not offered in the CI deploy
# dropdown: a converge run and the runner both authenticate as the deploy user,
# which is exactly what does not exist yet.
- name: Bootstrap a host for deployment
hosts: all
gather_facts: false
become: true
tags: [bootstrap]
vars:
deploy_user: deploy
deploy_pubkey_file: ~/.ssh/deploy_key.pub
# The compose_stack role escalates only when a stack_dirs entry sets owner
# or group, which is how a bind mount gets the uid its container runs as.
# metrics and openbao do that, so a host running either wants
# -e deploy_sudo=true. Still off by default: this grants NOPASSWD:ALL, and a
# host with no such stack has no reason to carry it.
deploy_sudo: false
tasks:
- name: Look for the public key on the control node
ansible.builtin.stat:
path: "{{ deploy_pubkey_file | expanduser }}"
register: bootstrap_pubkey
delegate_to: localhost
become: false
changed_when: false
# Before the user is half-created, rather than after.
- name: Verify the public key exists
ansible.builtin.assert:
that:
- bootstrap_pubkey.stat.exists | default(false)
fail_msg: >-
no public key at {{ deploy_pubkey_file }} —
run scripts/new-deploy-key.sh first
quiet: true
- name: Look for the docker group on the host
ansible.builtin.getent:
database: group
key: docker
fail_key: false
# Without this the user task fails with "Group docker does not exist", which
# is true and says nothing about the actual problem.
- name: Verify Docker is installed
ansible.builtin.assert:
that:
- ansible_facts.getent_group.docker is defined
fail_msg: >-
no docker group on {{ inventory_hostname }} — install Docker first
quiet: true
# The payload sync is ansible.posix.synchronize, which needs rsync at both
# ends. Installed rather than asserted, unlike Docker above: rsync is one
# package from the base repositories, where installing Docker means choosing
# repositories and versions and is a decision this playbook should not make
# on your behalf.
- name: Install rsync
ansible.builtin.package:
name: rsync
state: present
# In the docker group because the role runs compose unescalated: that is what
# keeps the bind mounts from being created root-owned.
- name: Create the deploy user
ansible.builtin.user:
name: "{{ deploy_user }}"
groups: docker
append: true
shell: /bin/bash
create_home: true
- name: Authorise the deploy key
ansible.posix.authorized_key:
user: "{{ deploy_user }}"
key: "{{ lookup('ansible.builtin.file', deploy_pubkey_file | expanduser) }}"
# Never exclusive. Rewriting the whole file would drop any other key that
# is currently somebody's only way onto this host.
exclusive: false
# The role creates {{ stack_root }}/<stack> unescalated, and /srv is
# root-owned, so without this the first deploy fails on a permission error
# that looks nothing like its cause.
- name: Create the stack root
ansible.builtin.file:
path: "{{ stack_root }}"
state: directory
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: "0755"
- name: Allow the deploy user to escalate
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ deploy_user }}"
content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
owner: root
group: root
mode: "0440"
# A malformed sudoers file can lock everyone out of a host permanently.
# validate runs visudo against the candidate and refuses to install it
# if it does not parse.
validate: visudo -cf %s
when: deploy_sudo | bool