semaphore/playbooks/bootstrap.yml

127 lines
5.0 KiB
YAML

---
# ============================================================================
# bootstrap.yml — prepare a fresh VPS/host for managed deployments
# ============================================================================
# Run this FIRST against any new host. Sequence:
# 1. Connection test (as root)
# 2. Clean up old-deployment clutter (explicit allowlist only — safe by default)
# 3. Create the `ansible` user with SSH key + password
# 4. Install the public key to /home/ansible/.ssh/authorized_keys
# 5. Verify the ansible user can log in and sudo
#
# Secrets come from Infisical (injected by Semaphore as env vars):
# SSH_PUBLIC_KEY - OpenSSH public key for the ansible user
# ANSIBLE_USER_PASS - password for the ansible user
# ANSIBLE_BECOME_ROOT_PASS - current root/sudo password on the target
#
# Usage (Semaphore Task Template):
# playbook: playbooks/bootstrap.yml
# inventory: inventory/hosts.yml
# limit: racknerd1
# Extra vars (optional): cleanup_paths='["/opt/old-thing","/opt/nuclio"]'
# ============================================================================
- name: Bootstrap managed host
hosts: "{{ target | default('racknerd1') }}"
become: true
gather_facts: true
vars:
managed_user: ansible
# Secrets pulled from Infisical env injection:
ansible_user_password: "{{ lookup('env', 'ANSIBLE_USER_PASS') }}"
managed_ssh_pubkey: "{{ lookup('env', 'SSH_PUBLIC_KEY') }}"
# SAFETY: cleanup removes ONLY paths explicitly listed here. Empty = deletes nothing.
# Override per-host from Semaphore (survey/extra-vars). NEVER a blind sweep.
cleanup_paths: []
tasks:
# ---------- STEP 1: connection test ----------
- name: "[1] Connection test — confirm we can reach the host and run as root"
ansible.builtin.command: id
register: idcheck
changed_when: false
- name: "[1] Report connection"
ansible.builtin.debug:
msg: "Connected to {{ inventory_hostname }} ({{ ansible_host | default('n/a') }}) as {{ idcheck.stdout }}"
- name: "[1] Assert we are root (become worked)"
ansible.builtin.assert:
that: "'uid=0' in idcheck.stdout"
fail_msg: "Not running as root — check become password (ANSIBLE_BECOME_ROOT_PASS)."
# ---------- STEP 2: cleanup (explicit allowlist only) ----------
- name: "[2] Show cleanup plan"
ansible.builtin.debug:
msg: >-
Cleanup paths to remove: {{ cleanup_paths | default([]) }}
{{ '(none — skipping)' if (cleanup_paths | length == 0) else '' }}
- name: "[2] Remove ONLY explicitly-listed old deployment paths"
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop: "{{ cleanup_paths | default([]) }}"
when: cleanup_paths | length > 0
# ---------- STEP 3 + 4: create ansible user + install key ----------
- name: "[3] Validate required secrets are present"
ansible.builtin.assert:
that:
- managed_ssh_pubkey | length > 0
- managed_ssh_pubkey.startswith('ssh-')
- ansible_user_password | length > 0
fail_msg: >-
Missing/invalid secrets. SSH_PUBLIC_KEY must be OpenSSH format (ssh-...)
and ANSIBLE_USER_PASS must be set (from Infisical).
- name: "[3] Create the '{{ managed_user }}' user"
ansible.builtin.user:
name: "{{ managed_user }}"
password: "{{ ansible_user_password | password_hash('sha512') }}"
shell: /bin/bash
groups: "{{ 'sudo' if ansible_os_family == 'Debian' else 'wheel' }}"
append: true
create_home: true
state: present
- name: "[4] Ensure ~/.ssh exists for {{ managed_user }}"
ansible.builtin.file:
path: "/home/{{ managed_user }}/.ssh"
state: directory
owner: "{{ managed_user }}"
group: "{{ managed_user }}"
mode: "0700"
- name: "[4] Install public key to {{ managed_user }} authorized_keys"
ansible.posix.authorized_key:
user: "{{ managed_user }}"
key: "{{ managed_ssh_pubkey }}"
state: present
exclusive: false
- name: "[3] Allow passwordless-or-password sudo for {{ managed_user }}"
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ managed_user }}"
content: "{{ managed_user }} ALL=(ALL) ALL\n"
mode: "0440"
validate: "visudo -cf %s"
# ---------- STEP 5: verify ----------
- name: "[5] Verify {{ managed_user }} exists and is in sudo group"
ansible.builtin.command: "id {{ managed_user }}"
register: userverify
changed_when: false
- name: "[5] Verify authorized_keys installed"
ansible.builtin.stat:
path: "/home/{{ managed_user }}/.ssh/authorized_keys"
register: akfile
- name: "[5] Bootstrap result"
ansible.builtin.debug:
msg:
- "User: {{ userverify.stdout }}"
- "authorized_keys present: {{ akfile.stat.exists }}"
- "Host {{ inventory_hostname }} is ready. Deploys can now run as '{{ managed_user }}'."