127 lines
5.0 KiB
YAML
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 }}'."
|