--- # ============================================================================ # 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 }}'."