From 4e72ddf4ef60582d4b0fcda09ad1d2a80022a3bd Mon Sep 17 00:00:00 2001 From: Pieter Date: Sat, 27 Dec 2025 14:13:15 +0100 Subject: [PATCH] Complete Ansible base configuration (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ansible/ansible.cfg | 3 +- ansible/playbooks/setup.yml | 40 +++++++++++ ansible/roles/docker/defaults/main.yml | 22 ++++++ ansible/roles/docker/handlers/main.yml | 7 ++ ansible/roles/docker/tasks/main.yml | 68 +++++++++++++++++++ ansible/roles/docker/templates/daemon.json.j2 | 1 + ansible/roles/traefik/defaults/main.yml | 19 ++++++ ansible/roles/traefik/handlers/main.yml | 7 ++ ansible/roles/traefik/tasks/main.yml | 37 ++++++++++ .../traefik/templates/docker-compose.yml.j2 | 36 ++++++++++ .../roles/traefik/templates/dynamic.yml.j2 | 21 ++++++ .../roles/traefik/templates/traefik.yml.j2 | 50 ++++++++++++++ docs/architecture-decisions.md | 8 +-- 13 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 ansible/playbooks/setup.yml create mode 100644 ansible/roles/docker/defaults/main.yml create mode 100644 ansible/roles/docker/handlers/main.yml create mode 100644 ansible/roles/docker/tasks/main.yml create mode 100644 ansible/roles/docker/templates/daemon.json.j2 create mode 100644 ansible/roles/traefik/defaults/main.yml create mode 100644 ansible/roles/traefik/handlers/main.yml create mode 100644 ansible/roles/traefik/tasks/main.yml create mode 100644 ansible/roles/traefik/templates/docker-compose.yml.j2 create mode 100644 ansible/roles/traefik/templates/dynamic.yml.j2 create mode 100644 ansible/roles/traefik/templates/traefik.yml.j2 diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index f787527..cbb5ea8 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -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 diff --git a/ansible/playbooks/setup.yml b/ansible/playbooks/setup.yml new file mode 100644 index 0000000..4f0f9bf --- /dev/null +++ b/ansible/playbooks/setup.yml @@ -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" diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000..7a7c23e --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -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" diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000..22e4bc2 --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,7 @@ +--- +# Handlers for docker role + +- name: Restart Docker + service: + name: docker + state: restarted diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000..0d7486a --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -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' diff --git a/ansible/roles/docker/templates/daemon.json.j2 b/ansible/roles/docker/templates/daemon.json.j2 new file mode 100644 index 0000000..062dc57 --- /dev/null +++ b/ansible/roles/docker/templates/daemon.json.j2 @@ -0,0 +1 @@ +{{ docker_daemon_options | to_nice_json }} diff --git a/ansible/roles/traefik/defaults/main.yml b/ansible/roles/traefik/defaults/main.yml new file mode 100644 index 0000000..41801e4 --- /dev/null +++ b/ansible/roles/traefik/defaults/main.yml @@ -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" diff --git a/ansible/roles/traefik/handlers/main.yml b/ansible/roles/traefik/handlers/main.yml new file mode 100644 index 0000000..0850b3a --- /dev/null +++ b/ansible/roles/traefik/handlers/main.yml @@ -0,0 +1,7 @@ +--- +# Handlers for traefik role + +- name: Restart Traefik + community.docker.docker_compose_v2: + project_src: /opt/docker/traefik + state: restarted diff --git a/ansible/roles/traefik/tasks/main.yml b/ansible/roles/traefik/tasks/main.yml new file mode 100644 index 0000000..ae31297 --- /dev/null +++ b/ansible/roles/traefik/tasks/main.yml @@ -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 diff --git a/ansible/roles/traefik/templates/docker-compose.yml.j2 b/ansible/roles/traefik/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..2379c4f --- /dev/null +++ b/ansible/roles/traefik/templates/docker-compose.yml.j2 @@ -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 diff --git a/ansible/roles/traefik/templates/dynamic.yml.j2 b/ansible/roles/traefik/templates/dynamic.yml.j2 new file mode 100644 index 0000000..2cc996d --- /dev/null +++ b/ansible/roles/traefik/templates/dynamic.yml.j2 @@ -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 diff --git a/ansible/roles/traefik/templates/traefik.yml.j2 b/ansible/roles/traefik/templates/traefik.yml.j2 new file mode 100644 index 0000000..326f9fa --- /dev/null +++ b/ansible/roles/traefik/templates/traefik.yml.j2 @@ -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 diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index eb2a85b..0ec3396 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -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 | ``` \ No newline at end of file