This commit is contained in:
nick committed 2026-07-15 19:03:35 -07:00
1 parent b745376a65
commit c49055d4dd
3 files changed
+126 -5

No files matched your search

+22 -1
View File
@@ -28,10 +28,31 @@ cd ansible
ansible-galaxy collection install -r requirements.yml 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 ## First-time setup
```bash ```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 cp group_vars/vpnhub.example.yml group_vars/vpnhub.yml
$EDITOR group_vars/vpnhub.yml # DC IPs, Duo keys, secrets $EDITOR group_vars/vpnhub.yml # DC IPs, Duo keys, secrets
ansible-vault encrypt group_vars/vpnhub.yml # both files are gitignored anyway ansible-vault encrypt group_vars/vpnhub.yml # both files are gitignored anyway
+98
View File
@@ -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
+6 -4
View File
@@ -5,7 +5,9 @@ all:
hosts: hosts:
racknerd: racknerd:
ansible_host: vpn.atlashorizon.net ansible_host: vpn.atlashorizon.net
ansible_user: root # Normal state — the user bootstrap.yml creates:
# Non-root instead? Use: ansible_user: ansible
# ansible_user: nick # ansible_ssh_private_key_file: ~/.ssh/id_ed25519
# ansible_become: true #
# bootstrap.yml ignores ansible_user and connects as root
# itself, so this file can point at "ansible" from day one.