Ansible project for server management
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Corinna Wieken 8ebf4a0802 Make firewall rule independent of existing roles
Signed-off-by: Corinna Wieken <corinna@wieken.org>
2026-08-15 18:51:40 +02:00
.idea Add forgejo role 2026-08-15 17:59:10 +02:00
files/keys Refine users role 2026-08-15 15:50:37 +02:00
inventory/development Make firewall rule independent of existing roles 2026-08-15 18:51:40 +02:00
playbooks Add caddy role for https/reverse proxy support 2026-08-15 18:27:59 +02:00
roles Make firewall rule independent of existing roles 2026-08-15 18:51:40 +02:00
.gitignore Initial commit 2026-08-15 15:44:33 +02:00
ansible.cfg Add forgejo role 2026-08-15 17:59:10 +02:00
README.md Add caddy role for https/reverse proxy support 2026-08-15 18:27:59 +02:00
requirements.yml Initial commit 2026-08-15 15:44:33 +02:00

ansible_dev

Ansible playbooks for managing wieken.dev and future development infrastructure (webservers, dataservers, aiservers).

Layout

inventory/development/     Inventory for the dev environment
  hosts.yml                Groups: devservers, webservers, dataservers, aiservers
  group_vars/all/          Vars applied to every host
    vars.yml                 Plaintext vars (safe to diff in git)
  group_vars/webservers/   Overrides for the webservers group (e.g. open 80/443)
  group_vars/devservers/  Overrides for the devservers group (Forgejo ports/secrets)
    vars.yml                 Plaintext vars (safe to diff in git)
    vault.yml.example        Template for secrets -> copy to vault.yml & encrypt
  host_vars/                Per-host overrides

playbooks/
  site.yml                  Runs everything below, in order
  common.yml                Baseline: common, users, ssh, firewall roles on `all`
  devservers.yml            Docker etc. for boxes used as dev environments
  webservers.yml             Group-specific plays (currently empty, add roles as built)
  dataservers.yml
  aiservers.yml

roles/
  common/         Minimum toolset (sudo, nano, curl, git, ...)
  users/          Create users/groups, deploy SSH keys, sudo config
  ssh/            sshd hardening (no root login, no password auth, ...)
  firewall/       ufw default-deny with explicitly allowed ports
  docker/         Docker Engine + docker compose plugin (any group can use it)
  caddy/          Shared reverse proxy + automatic Let's Encrypt TLS (devservers)
  forgejo/        Forgejo + Postgres via docker compose, proxied by caddy
  development/    Stub - not implemented yet (python, pixi, ...)
  letsencrypt/    Stub - not implemented yet

files/keys/       Public SSH keys (safe to commit)

New roles just get dropped into roles/ and added to the relevant group's role list (or playbooks/common.yml if it applies to every host).

Prerequisites: bootstrap the ansible control user

Ansible needs SSH access to a host before it can manage anything, so each new Debian host needs a one-time manual bootstrap: create a dedicated ansible user, grant it passwordless sudo (required for become to work non-interactively), and install your control machine's public key. The same key pair is reused for the ansible user across all hosts.

If you don't already have a dedicated key for this, generate one once:

ssh-keygen -t ed25519 -f ~/.ssh/ansible -C "ansible-control"

Log in to the target as root (or an existing sudo user) via whatever initial access your provider gives you, and run:

adduser --disabled-password --gecos "" ansible
usermod -aG sudo ansible
echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
chmod 440 /etc/sudoers.d/ansible
mkdir -p /home/ansible/.ssh
chmod 700 /home/ansible/.ssh
chown ansible:ansible /home/ansible/.ssh

Then, from your local machine, install the public key into that user's authorized_keys (run once per host, while you still have root access):

cat ~/.ssh/ansible.pub | ssh root@<host> \
  "cat >> /home/ansible/.ssh/authorized_keys && \
   chmod 600 /home/ansible/.ssh/authorized_keys && \
   chown ansible:ansible /home/ansible/.ssh/authorized_keys"

Verify it works and sudo is passwordless:

ssh -i ~/.ssh/ansible ansible@<host> sudo whoami
# should print: root, with no password prompt

inventory/development/hosts.yml already points every host at ~/.ssh/ansible via ansible_ssh_private_key_file. If you generated the key somewhere else, update that path (or override it per-host in host_vars/<host>.yml).

This ansible account is intentionally separate from the human accounts managed by roles/users below - it's bootstrapped once outside of Ansible and not touched by playbook runs.

First-time setup

ansible-galaxy collection install -r requirements.yml

Secrets (ansible-vault)

Secrets are split from plaintext vars: each group_vars/<group>/vars.yml references vault_-prefixed variables that live encrypted in that same directory's vault.yml. Currently only devservers/ (Forgejo DB password) has a vault file - group_vars/all/ has none since users_sudo_nopasswd: true means no password hashes are needed there right now.

cd inventory/development/group_vars/devservers
cp vault.yml.example vault.yml
ansible-vault encrypt vault.yml

Vault password file

ansible.cfg is already configured with vault_password_file = .vault_pass, so every ansible-playbook/ansible-vault command auto-decrypts without prompting - as long as that file exists. It's gitignored (never committed), so each machine you run this from needs its own copy:

echo -n 'your-vault-password-here' > .vault_pass
chmod 600 .vault_pass

Without this file, you'll see [ERROR]: Attempting to decrypt but no vault secrets found - either create .vault_pass as above, or pass --ask-vault-pass on every command instead.

Edit secrets later with:

ansible-vault edit inventory/development/group_vars/devservers/vault.yml

Users & SSH keys

Add accounts under users: in group_vars/all/vars.yml. Public keys go in files/keys/<name>.pub (safe to commit); password hashes go in vault.yml.

Before running roles/ssh (which disables password auth), verify new accounts can actually log in with their private key - keep your current session open and, in a second terminal, test:

ssh <name>@<host>

Reverse proxy (Caddy, devservers)

roles/caddy deploys Caddy via docker compose to /opt/caddy on every host in the devservers group. Caddy owns ports 80/443 exclusively and handles Let's Encrypt automatically (no certbot/cron - certs are requested and renewed by Caddy itself).

Apps aren't proxied by editing Caddy's own config. Instead:

  1. The app's container joins the shared proxy docker network (external, created by this role) in addition to its own private network(s).
  2. The app's role templates its own <name>.caddy file into caddy_sites_dir (/opt/caddy/sites/) with a reverse_proxy <container_name>:<port> block, then runs docker exec caddy caddy reload. roles/forgejo is the reference implementation for this pattern - copy it for future services.

This means adding a new subdomain/app later never touches roles/caddy, the firewall (only 80/443 are ever needed, for any number of apps), or DNS beyond one new CNAME - it's fully self-contained in the new app's role.

Forgejo (devservers)

roles/forgejo deploys Forgejo + Postgres via docker compose to /opt/forgejo on every host in the devservers group, proxied by Caddy (see above). Requires vault_forgejo_db_password to be set (see Secrets above).

  • HTTPS: https://git.<host>/ - the subdomain (git) is forgejo_subdomain in roles/forgejo/defaults/main.yml; the domain is inventory_hostname (e.g. wieken.dev), so this resolves automatically with zero manual config. Point a CNAME (or A record) for git.<host> at the server before running the playbook, or Let's Encrypt issuance fails. No host port is published for the web UI - it's only reachable through Caddy on the shared proxy network.
  • Git-over-SSH: port 23 (host) -> 22 (container), e.g. git clone ssh://git@<host>:23/<user>/<repo>.git - unaffected by Caddy, since git-SSH is a raw TCP protocol Caddy can't proxy.
  • Compose/env files are Ansible-managed. Every run ends with an unconditional docker compose up -d --remove-orphans, so the stack is reconciled to match the templates every time - config changes get applied, and containers that were manually stopped/removed get started again. restart: unless-stopped additionally covers container restarts across host reboots.

Running

# Full run
ansible-playbook playbooks/site.yml

# Just the security/users baseline
ansible-playbook playbooks/common.yml

# Just one host
ansible-playbook playbooks/site.yml --limit wieken.dev

# Dry run
ansible-playbook playbooks/site.yml --check --diff