From c49055d4dd878ee16b5608a6f8f30ac08e13c0d3 Mon Sep 17 00:00:00 2001 From: Chewbaccalakis Date: Wed, 15 Jul 2026 19:03:35 -0700 Subject: [PATCH] idk --- ansible/README.md | 23 +++++++- ansible/bootstrap.yml | 98 +++++++++++++++++++++++++++++++++++ ansible/inventory.example.yml | 10 ++-- 3 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 ansible/bootstrap.yml diff --git a/ansible/README.md b/ansible/README.md index b81e68e..13bbb0d 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -28,10 +28,31 @@ cd ansible ansible-galaxy collection install -r requirements.yml ``` +## Bootstrap (once per fresh VPS) + +`bootstrap.yml` connects as **root** (it overrides whatever `ansible_user` +the inventory has), creates the `ansible` automation user — SSH key only, +locked password, passwordless sudo — *proves the new user can log in and +sudo* in a second play, and only then locks sshd down: key-only auth +(`PasswordAuthentication no`) and **root SSH login disabled** +(`PermitRootLogin no`). Break-glass access after that is the provider's +console, not SSH: + +```bash +cp inventory.example.yml inventory.yml +ansible-playbook bootstrap.yml # root already has your key +ansible-playbook bootstrap.yml -k # root password login (needs sshpass) +# custom key or username: +ansible-playbook bootstrap.yml -e bootstrap_ssh_public_key_file=~/.ssh/vpnhub.pub +``` + +Everything after this runs as `ansible` (already the inventory example's +default). + ## First-time setup ```bash -cp inventory.example.yml inventory.yml # set host/user +cp inventory.example.yml inventory.yml # set host/user (if not done above) cp group_vars/vpnhub.example.yml group_vars/vpnhub.yml $EDITOR group_vars/vpnhub.yml # DC IPs, Duo keys, secrets ansible-vault encrypt group_vars/vpnhub.yml # both files are gitignored anyway diff --git a/ansible/bootstrap.yml b/ansible/bootstrap.yml new file mode 100644 index 0000000..2e5dcaa --- /dev/null +++ b/ansible/bootstrap.yml @@ -0,0 +1,98 @@ +--- +# 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 # root already has your key +# ansible-playbook bootstrap.yml -k # root still uses a password +# # (needs sshpass installed) +# +# 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: + ansible_user: root # play var — outranks inventory on purpose + bootstrap_user: ansible + bootstrap_ssh_public_key_file: "~/.ssh/id_ed25519.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 diff --git a/ansible/inventory.example.yml b/ansible/inventory.example.yml index 4a3b976..acf057a 100644 --- a/ansible/inventory.example.yml +++ b/ansible/inventory.example.yml @@ -5,7 +5,9 @@ all: hosts: racknerd: ansible_host: vpn.atlashorizon.net - ansible_user: root - # Non-root instead? Use: - # ansible_user: nick - # ansible_become: true + # Normal state — the user bootstrap.yml creates: + ansible_user: ansible + # ansible_ssh_private_key_file: ~/.ssh/id_ed25519 + # + # bootstrap.yml ignores ansible_user and connects as root + # itself, so this file can point at "ansible" from day one.