Complete Ansible base configuration (#2)

Completed Issue #2: Ansible Base Configuration

All objectives met:
-  Hetzner Cloud dynamic inventory (hcloud plugin)
-  Common role (SSH hardening, UFW firewall, fail2ban, auto-updates)
-  Docker role (Docker Engine + Compose + networks)
-  Traefik role (reverse proxy with Let's Encrypt SSL)
-  Setup playbook (orchestrates all base roles)
-  Successfully tested on live test server (91.99.210.204)

Additional improvements:
- Fixed ansible.cfg for Ansible 2.20+ compatibility
- Updated ADR dates to 2025
- All roles follow Infrastructure Agent patterns

Test Results:
- SSH hardening applied (key-only auth)
- UFW firewall active (ports 22, 80, 443)
- Fail2ban protecting SSH
- Automatic security updates enabled
- Docker running with traefik network
- Traefik deployed and ready for SSL

Files added:
- ansible/playbooks/setup.yml
- ansible/roles/docker/* (complete)
- ansible/roles/traefik/* (complete)

Closes #2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pieter 2025-12-27 14:13:15 +01:00
parent 171cbfbb32
commit 4e72ddf4ef
13 changed files with 314 additions and 5 deletions

View file

@ -12,7 +12,8 @@ fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
# Output
stdout_callback = yaml
stdout_callback = default
result_format = yaml
bin_ansible_callbacks = True
display_skipped_hosts = False

View file

@ -0,0 +1,40 @@
---
# Initial server setup playbook
# Provisions base infrastructure: hardening, Docker, Traefik
- name: Setup base infrastructure
hosts: all
become: yes
vars:
# Override these in group_vars or host_vars
traefik_acme_email: "admin@postxsociety.cloud"
pre_tasks:
- name: Wait for system to be ready
wait_for_connection:
timeout: 300
- name: Gather facts
setup:
roles:
- role: common
tags: ['common', 'security']
- role: docker
tags: ['docker']
- role: traefik
tags: ['traefik', 'proxy']
post_tasks:
- name: Display server information
debug:
msg:
- "✅ Server setup complete!"
- "Hostname: {{ ansible_hostname }}"
- "IP Address: {{ ansible_default_ipv4.address }}"
- "SSH hardened, UFW enabled, fail2ban active"
- "Docker installed and running"
- "Traefik managing SSL certificates automatically"

View file

@ -0,0 +1,22 @@
---
# Default variables for docker role
# Docker version (use 'latest' or pin specific version)
docker_edition: "ce"
docker_install_compose: true
# Docker daemon configuration
docker_daemon_options:
log-driver: "json-file"
log-opts:
max-size: "10m"
max-file: "3"
storage-driver: "overlay2"
# Docker Compose version
docker_compose_version: "2.24.0"
# Docker networks to create
docker_networks:
- name: "traefik"
driver: "bridge"

View file

@ -0,0 +1,7 @@
---
# Handlers for docker role
- name: Restart Docker
service:
name: docker
state: restarted

View file

@ -0,0 +1,68 @@
---
# Main tasks for docker role - install Docker and Docker Compose
- name: Install prerequisites
apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: yes
- name: Add Docker GPG key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add Docker repository
apt_repository:
repo: "deb [arch=arm64,amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
filename: docker
- name: Install Docker Engine
apt:
name:
- docker-{{ docker_edition }}
- docker-{{ docker_edition }}-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: yes
notify: Restart Docker
- name: Create Docker daemon configuration directory
file:
path: /etc/docker
state: directory
mode: '0755'
- name: Configure Docker daemon
template:
src: daemon.json.j2
dest: /etc/docker/daemon.json
mode: '0644'
notify: Restart Docker
- name: Create Docker networks
community.docker.docker_network:
name: "{{ item.name }}"
driver: "{{ item.driver }}"
state: present
loop: "{{ docker_networks }}"
- name: Ensure Docker is running and enabled
service:
name: docker
state: started
enabled: yes
- name: Create /opt/docker directory for compose files
file:
path: /opt/docker
state: directory
mode: '0755'

View file

@ -0,0 +1 @@
{{ docker_daemon_options | to_nice_json }}

View file

@ -0,0 +1,19 @@
---
# Default variables for traefik role
# Traefik version
traefik_version: "v3.0"
# Let's Encrypt configuration
traefik_acme_email: "admin@example.com" # Override this!
traefik_acme_staging: false # Set to true for testing
# Dashboard configuration
traefik_dashboard_enabled: false
traefik_dashboard_domain: "traefik.example.com"
# Network
traefik_network: "traefik"
# Docker socket (for auto-discovery)
traefik_docker_socket: "/var/run/docker.sock"

View file

@ -0,0 +1,7 @@
---
# Handlers for traefik role
- name: Restart Traefik
community.docker.docker_compose_v2:
project_src: /opt/docker/traefik
state: restarted

View file

@ -0,0 +1,37 @@
---
# Main tasks for traefik role - deploy Traefik reverse proxy
- name: Create Traefik directories
file:
path: "{{ item }}"
state: directory
mode: '0755'
loop:
- /opt/docker/traefik
- /opt/docker/traefik/letsencrypt
- name: Deploy Traefik static configuration
template:
src: traefik.yml.j2
dest: /opt/docker/traefik/traefik.yml
mode: '0644'
notify: Restart Traefik
- name: Deploy Traefik dynamic configuration
template:
src: dynamic.yml.j2
dest: /opt/docker/traefik/dynamic.yml
mode: '0644'
notify: Restart Traefik
- name: Deploy Traefik docker-compose file
template:
src: docker-compose.yml.j2
dest: /opt/docker/traefik/docker-compose.yml
mode: '0644'
notify: Restart Traefik
- name: Start Traefik via Docker Compose
community.docker.docker_compose_v2:
project_src: /opt/docker/traefik
state: present

View file

@ -0,0 +1,36 @@
# Traefik Reverse Proxy
# Managed by Ansible - do not edit manually
services:
traefik:
image: traefik:{{ traefik_version }}
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
{% if traefik_dashboard_enabled %}
- "8080:8080"
{% endif %}
volumes:
- /etc/localtime:/etc/localtime:ro
- {{ traefik_docker_socket }}:{{ traefik_docker_socket }}:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
- ./letsencrypt:/letsencrypt
networks:
- {{ traefik_network }}
{% if traefik_dashboard_enabled %}
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`{{ traefik_dashboard_domain }}`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
{% endif %}
networks:
{{ traefik_network }}:
external: true

View file

@ -0,0 +1,21 @@
# Traefik dynamic configuration
# Managed by Ansible - do not edit manually
http:
middlewares:
# Security headers
security-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
customFrameOptionsValue: "SAMEORIGIN"
# Rate limiting
rate-limit:
rateLimit:
average: 100
burst: 200

View file

@ -0,0 +1,50 @@
# Traefik static configuration
# Managed by Ansible - do not edit manually
api:
dashboard: {{ traefik_dashboard_enabled | lower }}
{% if traefik_dashboard_enabled %}
insecure: false
{% endif %}
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
providers:
docker:
endpoint: "unix://{{ traefik_docker_socket }}"
exposedByDefault: false
network: {{ traefik_network }}
file:
filename: /etc/traefik/dynamic.yml
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: {{ traefik_acme_email }}
storage: /letsencrypt/acme.json
{% if traefik_acme_staging %}
caServer: https://acme-staging-v02.api.letsencrypt.org/directory
{% endif %}
httpChallenge:
entryPoint: web
log:
level: INFO
accessLog:
filePath: /var/log/traefik/access.log

View file

@ -2,9 +2,9 @@
## Post-X Society Multi-Tenant VPS Platform
**Document Status:** Living document
**Created:** December 2024
**Last Updated:** December 2024
**Document Status:** Living document
**Created:** December 2024
**Last Updated:** December 2025
---
@ -848,5 +848,5 @@ pipx inject ansible requests python-dateutil
| 2024-12 | Switched from Terraform to OpenTofu (licensing concerns) | Pieter / Claude |
| 2024-12 | Switched from HashiCorp Vault to SOPS + Age (simplicity, open source) | Pieter / Claude |
| 2024-12 | Switched from Keycloak to Zitadel (Swiss company, GDPR jurisdiction) | Pieter / Claude |
| 2024-12 | Adopted pipx for isolated Python tool environments (Ansible) | Pieter / Claude |
| 2025-12 | Adopted pipx for isolated Python tool environments (Ansible) | Pieter / Claude |
```