--- # One-job playbook: connect as root, create the automation user that every # later run uses — key-only auth, passwordless sudo — verify it actually # works, then lock sshd down to keys. Run once per fresh VPS: # # ansible-playbook bootstrap.yml # # It prompts for the root SSH password (needs sshpass on the control # node). If root already authenticates with your key, just press Enter. # # Afterwards set `ansible_user: ansible` in inventory.yml and never # connect as root again. Overridable with -e: # bootstrap_user=ansible # bootstrap_ssh_public_key_file=~/.ssh/id_ed25519.pub # bootstrap_harden_ssh=true # # Safe by construction: play 2 logs in AS the new user and proves key + # sudo work; sshd is only hardened in play 3, which never runs if play 2 # fails. Play 3 disables root SSH login entirely — from then on the only # way in is the automation user's key (console access via the RackNerd # panel remains the break-glass path). - name: Create the automation user (connects as root) hosts: vpnhub gather_facts: false vars_prompt: - name: bootstrap_root_password prompt: "Root SSH password (Enter if root already uses your key)" private: true default: "" vars: ansible_user: root # play var — outranks inventory on purpose ansible_password: "{{ bootstrap_root_password }}" bootstrap_user: ansible bootstrap_ssh_public_key_file: "~/.ssh/togo-ansible.pub" tasks: - name: Create the user (no password hash set, account password locked) ansible.builtin.user: name: "{{ bootstrap_user }}" shell: /bin/bash create_home: true password_lock: true - name: Install the SSH public key ansible.posix.authorized_key: user: "{{ bootstrap_user }}" key: "{{ lookup('file', bootstrap_ssh_public_key_file) }}" - name: Grant passwordless sudo ansible.builtin.copy: dest: "/etc/sudoers.d/{{ bootstrap_user }}" content: "{{ bootstrap_user }} ALL=(ALL) NOPASSWD:ALL\n" owner: root group: root mode: "0440" validate: /usr/sbin/visudo -cf %s - name: Prove the new user works before touching sshd hosts: vpnhub gather_facts: false become: true vars: bootstrap_user: ansible ansible_user: "{{ bootstrap_user }}" tasks: - name: Key login and passwordless sudo both work ansible.builtin.command: whoami register: bootstrap_whoami changed_when: false failed_when: bootstrap_whoami.stdout != "root" - name: Lock sshd down to key-only auth, no root login hosts: vpnhub gather_facts: false become: true vars: bootstrap_user: ansible ansible_user: "{{ bootstrap_user }}" bootstrap_harden_ssh: true handlers: - name: restart sshd ansible.builtin.service: name: ssh state: restarted tasks: - name: Key-only sshd drop-in # Named 01-* on purpose: sshd honors the FIRST value it reads per # keyword, and cloud-init images ship 50-cloud-init.conf that may # set PasswordAuthentication yes — we must sort before it. ansible.builtin.copy: dest: /etc/ssh/sshd_config.d/01-vpnhub.conf content: | PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin no owner: root group: root mode: "0644" when: bootstrap_harden_ssh | bool notify: restart sshd - name: Validate full sshd config before the restart fires ansible.builtin.command: /usr/sbin/sshd -t when: bootstrap_harden_ssh | bool changed_when: false