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:
parent
171cbfbb32
commit
4e72ddf4ef
13 changed files with 314 additions and 5 deletions
|
|
@ -12,7 +12,8 @@ fact_caching_connection = /tmp/ansible_facts
|
||||||
fact_caching_timeout = 86400
|
fact_caching_timeout = 86400
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
stdout_callback = yaml
|
stdout_callback = default
|
||||||
|
result_format = yaml
|
||||||
bin_ansible_callbacks = True
|
bin_ansible_callbacks = True
|
||||||
display_skipped_hosts = False
|
display_skipped_hosts = False
|
||||||
|
|
||||||
|
|
|
||||||
40
ansible/playbooks/setup.yml
Normal file
40
ansible/playbooks/setup.yml
Normal 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"
|
||||||
22
ansible/roles/docker/defaults/main.yml
Normal file
22
ansible/roles/docker/defaults/main.yml
Normal 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"
|
||||||
7
ansible/roles/docker/handlers/main.yml
Normal file
7
ansible/roles/docker/handlers/main.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
# Handlers for docker role
|
||||||
|
|
||||||
|
- name: Restart Docker
|
||||||
|
service:
|
||||||
|
name: docker
|
||||||
|
state: restarted
|
||||||
68
ansible/roles/docker/tasks/main.yml
Normal file
68
ansible/roles/docker/tasks/main.yml
Normal 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'
|
||||||
1
ansible/roles/docker/templates/daemon.json.j2
Normal file
1
ansible/roles/docker/templates/daemon.json.j2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{{ docker_daemon_options | to_nice_json }}
|
||||||
19
ansible/roles/traefik/defaults/main.yml
Normal file
19
ansible/roles/traefik/defaults/main.yml
Normal 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"
|
||||||
7
ansible/roles/traefik/handlers/main.yml
Normal file
7
ansible/roles/traefik/handlers/main.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
# Handlers for traefik role
|
||||||
|
|
||||||
|
- name: Restart Traefik
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: /opt/docker/traefik
|
||||||
|
state: restarted
|
||||||
37
ansible/roles/traefik/tasks/main.yml
Normal file
37
ansible/roles/traefik/tasks/main.yml
Normal 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
|
||||||
36
ansible/roles/traefik/templates/docker-compose.yml.j2
Normal file
36
ansible/roles/traefik/templates/docker-compose.yml.j2
Normal 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
|
||||||
21
ansible/roles/traefik/templates/dynamic.yml.j2
Normal file
21
ansible/roles/traefik/templates/dynamic.yml.j2
Normal 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
|
||||||
50
ansible/roles/traefik/templates/traefik.yml.j2
Normal file
50
ansible/roles/traefik/templates/traefik.yml.j2
Normal 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
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
**Document Status:** Living document
|
**Document Status:** Living document
|
||||||
**Created:** December 2024
|
**Created:** December 2024
|
||||||
**Last Updated:** 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 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 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 | 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 |
|
||||||
```
|
```
|
||||||
Loading…
Add table
Reference in a new issue