Ansible role for managing this server
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Corinna Wieken 19568b6718 Initial roles
Signed-off-by: Corinna Wieken <corinna@wieken.org>
2026-10-30 20:44:59 +01:00
files/keys Initial roles 2026-10-30 20:44:59 +01:00
inventory Initial roles 2026-10-30 20:44:59 +01:00
playbooks Initial roles 2026-10-30 20:44:59 +01:00
roles Initial roles 2026-10-30 20:44:59 +01:00
.gitignore Initial roles 2026-10-30 20:44:59 +01:00
ansible.cfg Initial roles 2026-10-30 20:44:59 +01:00
README.md Initial roles 2026-10-30 20:44:59 +01:00
requirements.yml Initial roles 2026-10-30 20:44:59 +01:00

devserver

Ansible setup for development, business and home servers.

Layout

inventory/
  hosts.yml              Groups: devservers, businessservers, homeservers
  group_vars/all/        Users, sudo
  group_vars/devservers/ Docker users, Forgejo; vault.yml.example for secrets
  host_vars/             Per-host overrides (e.g. personal users)

playbooks/
  site.yml               Everything, in order
  common.yml             Every host: users, security, firewall, docker
  devservers.yml         Forgejo (+ Caddy)
  businessservers.yml    Empty
  homeservers.yml        Empty

roles/
  users/     admin, developer and per-host personal accounts, SSH keys, sudo
  security/  SSH hardening: key-only login, no root login
  firewall/  ufw, default-deny incoming; allow_ports.yml for other roles
  docker/    Docker Engine + compose (Debian/Ubuntu, Arch); compose_up.yml for other roles
  caddy/     Reverse proxy + automatic Let's Encrypt (docker compose)
  forgejo/   Forgejo + Postgres (docker compose), served at git.<host> through Caddy

files/keys/  Public SSH keys: <name>.pub

Prerequisites

Control user on each host

Ansible connects as a dedicated ansible user with passwordless sudo, created once by hand per host (this account is not managed by roles/users). As root on the target:

useradd --create-home --shell /bin/bash ansible
passwd --lock ansible
echo 'ansible ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ansible
chmod 440 /etc/sudoers.d/ansible
install -d -m 700 -o ansible -g ansible /home/ansible/.ssh

Install your key (ssh-keygen -t ed25519 -f ~/.ssh/ansible if you don't have one):

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"
ssh -i ~/.ssh/ansible ansible@<host> sudo whoami   # root, no prompt

sudo must be installed on the target. The key path is set in inventory/hosts.yml.

Collections

ansible-galaxy collection install -r requirements.yml

DNS

Every proxied app is served at <subdomain>.<inventory_hostname> (for Forgejo git.<host>). Point a DNS record (A/AAAA or CNAME) for each at the server before the first run. The run checks this itself, see "Reverse proxy and apps".

Secrets

echo -n 'your-vault-password' > .vault_pass && chmod 600 .vault_pass
cd inventory/group_vars/devservers
cp vault.yml.example vault.yml     # set vault_forgejo_db_password
ansible-vault encrypt vault.yml

ansible.cfg points at .vault_pass; Ansible won't start without that file.

Running

ansible-playbook playbooks/site.yml                    # everything
ansible-playbook playbooks/common.yml                  # users, security, firewall, docker only
ansible-playbook playbooks/site.yml --limit <host>
ansible-playbook playbooks/site.yml --check --diff

Users

  • admin (group admin, sudo) and developer (group developer, no sudo) exist on every host.

  • Personal accounts come from users_personal. The default is set in inventory/group_vars/all/vars.yml; override per host in host_vars/<host>.yml:

    users_personal:
      - name: first_user
      - name: second_user
        groups: [developer]   # default: admin + developer
    
  • Keys are read from files/keys/<name>.pub (or set ssh_keys: on the entry). authorized_keys is managed exclusively: keys not declared are removed.

  • No passwords are set, so sudo is passwordless (users_sudo_nopasswd).

  • On devservers all of these accounts are in the docker group (root-equivalent), see docker_users.

Security

roles/security writes /etc/ssh/sshd_config.d/00-hardening.conf:

  • Public-key authentication only (AuthenticationMethods publickey); password and keyboard-interactive login are off.
  • Root login off (security_ssh_permit_root_login), X11 forwarding off, MaxAuthTries 3.
  • Optional security_ssh_allow_users / security_ssh_allow_groups. The default is no restriction. If you set security_ssh_allow_users, include the ansible user.

The role runs after users, so the keys are in place before passwords are disabled. It checks the effective configuration (sshd -T) before reloading sshd, so a config that doesn't take effect fails the run instead of silently doing nothing. The ansible user must have its key installed (see Prerequisites), or you lock yourself out. The SSH port is not changed here; the firewall follows the port Ansible connects on.

Firewall and Docker

roles/firewall uses ufw: incoming denied, SSH (the port Ansible connects on) allowed. Roles open their own ports through roles/firewall/tasks/allow_ports.yml (Caddy: 80/443, Forgejo: git-ssh port).

Docker-published ports bypass ufw, so the rules document intent but don't enforce it. Only publish ports a service needs. Everything behind Caddy publishes nothing and is reached over the shared proxy network.

Reverse proxy and apps

roles/caddy runs Caddy in /opt/caddy and creates the shared proxy docker network. An app joins that network and drops <name>.caddy into /opt/caddy/etc/sites/ (see roles/forgejo); the app role declares caddy in its meta/main.yml.

App roles also include two checks from roles/caddy, so a missing DNS record or failed certificate issuance fails the run instead of going unnoticed:

  • check_dns.yml (before the site file is deployed): the domain must resolve, and only to addresses of the target (its interfaces, or the address Ansible connects to). Disable with caddy_check_dns: false, e.g. behind a CDN proxy.
  • verify_https.yml (after the reload): https://<domain>/ must present a valid certificate when requested from the control node, retried for up to caddy_verify_retries * caddy_verify_delay seconds (3 minutes by default). Any HTTP status counts, only connection and TLS errors fail. Disable with caddy_verify_https: false, e.g. if the control node can't reach the server. Skipped in --check mode.

Forgejo

  • Web: https://<forgejo_subdomain>.<host>/ (subdomain default git).
  • Git over SSH: ssh://git@<forgejo_subdomain>.<host>:<forgejo_ssh_port>/<user>/<repo>.git (set in group_vars/devservers; the system sshd keeps its own port).
  • Open registration is off (forgejo_disable_registration). Complete the install wizard on first visit to create the admin account.
  • Files live in /opt/forgejo. Every run reconciles the stack with docker compose up -d.

Not included yet

SSH hardening (root login, password auth), base packages and backups.