feat: add PHP ini management system and update infrastructure configs
- Add PHP ini management classes (Access, IniDirective, IniKey, PhpIni) - Update deployment configurations (Wireguard, Traefik, Monitoring) - Add DNS stack and Ansible role - Add deployment debugging playbooks - Update framework components (FilePath, RedisConnectionPool) - Update .gitignore and documentation
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -58,3 +58,5 @@ cookies_new.txt
|
|||||||
playwright-report/
|
playwright-report/
|
||||||
test-results/
|
test-results/
|
||||||
.playwright/
|
.playwright/
|
||||||
|
# WireGuard client configs (generated locally)
|
||||||
|
deployment/ansible/wireguard-clients/
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf"
|
wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf"
|
||||||
wireguard_client_configs_path: "/etc/wireguard/clients"
|
wireguard_client_configs_path: "/etc/wireguard/clients"
|
||||||
wireguard_local_client_configs_dir: "{{ playbook_dir }}/../wireguard-clients"
|
wireguard_local_client_configs_dir: "{{ playbook_dir }}/../wireguard-clients"
|
||||||
|
wireguard_dns_servers: []
|
||||||
|
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Set WireGuard network
|
- name: Set WireGuard network
|
||||||
@@ -61,6 +62,11 @@
|
|||||||
set_fact:
|
set_fact:
|
||||||
server_vpn_ip: "{{ (wireguard_server_config_read.content | b64decode | regex_search('Address = ([0-9.]+)', '\\1')) | first | default('10.8.0.1') }}"
|
server_vpn_ip: "{{ (wireguard_server_config_read.content | b64decode | regex_search('Address = ([0-9.]+)', '\\1')) | first | default('10.8.0.1') }}"
|
||||||
|
|
||||||
|
- name: Set default DNS servers if not provided
|
||||||
|
set_fact:
|
||||||
|
wireguard_dns_servers: "{{ [server_vpn_ip] }}"
|
||||||
|
when: wireguard_dns_servers | length == 0
|
||||||
|
|
||||||
- name: Extract WireGuard server IP octets
|
- name: Extract WireGuard server IP octets
|
||||||
set_fact:
|
set_fact:
|
||||||
wireguard_server_ip_octets: "{{ server_vpn_ip.split('.') }}"
|
wireguard_server_ip_octets: "{{ server_vpn_ip.split('.') }}"
|
||||||
|
|||||||
147
deployment/ansible/playbooks/check-production-status.yml
Normal file
147
deployment/ansible/playbooks/check-production-status.yml
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
- name: Check Production Server Status
|
||||||
|
hosts: production
|
||||||
|
gather_facts: yes
|
||||||
|
become: no
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Check server uptime and basic info
|
||||||
|
shell: |
|
||||||
|
echo "=== Server Uptime ==="
|
||||||
|
uptime
|
||||||
|
echo ""
|
||||||
|
echo "=== Disk Space ==="
|
||||||
|
df -h
|
||||||
|
echo ""
|
||||||
|
echo "=== Memory Usage ==="
|
||||||
|
free -h
|
||||||
|
echo ""
|
||||||
|
echo "=== Docker Status ==="
|
||||||
|
docker --version || echo "Docker not found"
|
||||||
|
docker ps || echo "Docker not running"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: server_info
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display server info
|
||||||
|
debug:
|
||||||
|
msg: "{{ server_info.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check all Docker stacks status
|
||||||
|
shell: |
|
||||||
|
echo "=== Traefik Stack ==="
|
||||||
|
cd ~/deployment/stacks/traefik && docker compose ps 2>&1 || echo "Traefik stack not found or not running"
|
||||||
|
echo ""
|
||||||
|
echo "=== Application Stack ==="
|
||||||
|
cd ~/deployment/stacks/application && docker compose ps 2>&1 || echo "Application stack not found or not running"
|
||||||
|
echo ""
|
||||||
|
echo "=== PostgreSQL Stack ==="
|
||||||
|
cd ~/deployment/stacks/postgresql && docker compose ps 2>&1 || echo "PostgreSQL stack not found or not running"
|
||||||
|
echo ""
|
||||||
|
echo "=== Monitoring Stack ==="
|
||||||
|
cd ~/deployment/stacks/monitoring && docker compose ps 2>&1 || echo "Monitoring stack not found or not running"
|
||||||
|
echo ""
|
||||||
|
echo "=== Gitea Stack ==="
|
||||||
|
cd ~/deployment/stacks/gitea && docker compose ps 2>&1 || echo "Gitea stack not found or not running"
|
||||||
|
echo ""
|
||||||
|
echo "=== Registry Stack ==="
|
||||||
|
cd ~/deployment/stacks/registry && docker compose ps 2>&1 || echo "Registry stack not found or not running"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: stacks_status
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display stacks status
|
||||||
|
debug:
|
||||||
|
msg: "{{ stacks_status.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check Traefik logs for errors
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/traefik
|
||||||
|
echo "=== Traefik Logs (Last 30 lines) ==="
|
||||||
|
docker compose logs --tail=30 traefik 2>&1 | tail -30 || echo "Could not read Traefik logs"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: traefik_logs
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display Traefik logs
|
||||||
|
debug:
|
||||||
|
msg: "{{ traefik_logs.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check Application stack logs
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/application
|
||||||
|
echo "=== Application Nginx Logs (Last 20 lines) ==="
|
||||||
|
docker compose logs --tail=20 web 2>&1 | tail -20 || echo "Could not read web logs"
|
||||||
|
echo ""
|
||||||
|
echo "=== Application PHP Logs (Last 20 lines) ==="
|
||||||
|
docker compose logs --tail=20 php 2>&1 | tail -20 || echo "Could not read PHP logs"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: app_logs
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display application logs
|
||||||
|
debug:
|
||||||
|
msg: "{{ app_logs.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Test HTTP connectivity
|
||||||
|
shell: |
|
||||||
|
echo "=== Testing HTTP Connectivity ==="
|
||||||
|
echo "Test 1: HTTPS to michaelschiemer.de"
|
||||||
|
curl -k -H "User-Agent: Mozilla/5.0" -s -o /dev/null -w "HTTP %{http_code}\n" https://michaelschiemer.de/health || echo "Connection failed"
|
||||||
|
echo ""
|
||||||
|
echo "Test 2: Direct localhost"
|
||||||
|
curl -k -H "User-Agent: Mozilla/5.0" -s -o /dev/null -w "HTTP %{http_code}\n" https://localhost/health || echo "Connection failed"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: http_tests
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display HTTP test results
|
||||||
|
debug:
|
||||||
|
msg: "{{ http_tests.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check network connectivity
|
||||||
|
shell: |
|
||||||
|
echo "=== Network Interfaces ==="
|
||||||
|
ip addr show | grep -E "(inet |state)" | head -10
|
||||||
|
echo ""
|
||||||
|
echo "=== Docker Networks ==="
|
||||||
|
docker network ls
|
||||||
|
echo ""
|
||||||
|
echo "=== Traefik Network Connectivity ==="
|
||||||
|
docker network inspect traefik-public 2>&1 | grep -E "(Name|Subnet|Containers)" | head -10 || echo "Traefik network not found"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: network_info
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display network info
|
||||||
|
debug:
|
||||||
|
msg: "{{ network_info.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check firewall status
|
||||||
|
shell: |
|
||||||
|
echo "=== Firewall Status ==="
|
||||||
|
sudo ufw status || echo "UFW not installed or not configured"
|
||||||
|
echo ""
|
||||||
|
echo "=== Listening Ports ==="
|
||||||
|
sudo netstat -tlnp | grep -E "(80|443|8080|3000)" | head -10 || ss -tlnp | grep -E "(80|443|8080|3000)" | head -10 || echo "Could not check listening ports"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: firewall_info
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display firewall info
|
||||||
|
debug:
|
||||||
|
msg: "{{ firewall_info.stdout_lines }}"
|
||||||
49
deployment/ansible/playbooks/check-staging-500-error.yml
Normal file
49
deployment/ansible/playbooks/check-staging-500-error.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
- name: Check Staging 500 Error
|
||||||
|
hosts: production
|
||||||
|
gather_facts: yes
|
||||||
|
become: no
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Get recent PHP errors from staging-app
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/staging
|
||||||
|
echo "=== Recent PHP errors (last 50 lines) ==="
|
||||||
|
docker compose exec -T staging-app tail -100 /var/www/html/storage/logs/php-errors.log 2>&1 | tail -50
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: php_errors
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display PHP errors
|
||||||
|
debug:
|
||||||
|
msg: "{{ php_errors.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Get docker compose logs for staging-app
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/staging
|
||||||
|
echo "=== Recent staging-app container logs ==="
|
||||||
|
docker compose logs --tail=50 staging-app 2>&1 | tail -50
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: container_logs
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display container logs
|
||||||
|
debug:
|
||||||
|
msg: "{{ container_logs.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Test health endpoint
|
||||||
|
shell: |
|
||||||
|
curl -H "User-Agent: Mozilla/5.0" -s https://staging.michaelschiemer.de/health 2>&1
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: health_test
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display health endpoint result
|
||||||
|
debug:
|
||||||
|
msg: "{{ health_test.stdout }}"
|
||||||
125
deployment/ansible/playbooks/debug-grafana-403.yml
Normal file
125
deployment/ansible/playbooks/debug-grafana-403.yml
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
- name: Debug Grafana 403 Error
|
||||||
|
hosts: production
|
||||||
|
gather_facts: yes
|
||||||
|
become: no
|
||||||
|
|
||||||
|
# This playbook requires the production inventory file
|
||||||
|
# Run with: ansible-playbook -i ../inventory/production.yml debug-grafana-403.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Check Traefik logs for recent Grafana access attempts
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/traefik
|
||||||
|
echo "=== Recent Traefik Access Logs (last 50 lines with grafana) ==="
|
||||||
|
docker compose logs --tail=100 traefik 2>&1 | grep -i grafana | tail -50 || echo "No grafana entries found"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: traefik_logs
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display Traefik logs
|
||||||
|
debug:
|
||||||
|
msg: "{{ traefik_logs.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check Traefik access log file
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/traefik
|
||||||
|
echo "=== Recent Traefik Access Log (last 50 lines) ==="
|
||||||
|
tail -50 logs/access.log 2>&1 | tail -50 || echo "Access log not found"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: access_log
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display access log
|
||||||
|
debug:
|
||||||
|
msg: "{{ access_log.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check Grafana container status
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/monitoring
|
||||||
|
docker compose ps grafana
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: grafana_status
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display Grafana status
|
||||||
|
debug:
|
||||||
|
msg: "{{ grafana_status.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check Grafana Traefik labels
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/monitoring
|
||||||
|
docker compose config | grep -A 20 "grafana:" | grep -E "(ipwhitelist|middleware|sourcerange)" || echo "No IP whitelist labels found"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: grafana_labels
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display Grafana labels
|
||||||
|
debug:
|
||||||
|
msg: "{{ grafana_labels.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check CoreDNS configuration
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/dns
|
||||||
|
echo "=== CoreDNS Corefile ==="
|
||||||
|
cat Corefile 2>&1 || echo "Corefile not found"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: coredns_config
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display CoreDNS configuration
|
||||||
|
debug:
|
||||||
|
msg: "{{ coredns_config.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check monitoring stack environment variables
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/monitoring
|
||||||
|
echo "=== MONITORING_VPN_IP_WHITELIST ==="
|
||||||
|
grep MONITORING_VPN_IP_WHITELIST .env 2>&1 || echo "Variable not found in .env"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: monitoring_env
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display monitoring environment
|
||||||
|
debug:
|
||||||
|
msg: "{{ monitoring_env.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Test DNS resolution for grafana.michaelschiemer.de
|
||||||
|
shell: |
|
||||||
|
echo "=== DNS Resolution Test ==="
|
||||||
|
dig +short grafana.michaelschiemer.de @10.8.0.1 2>&1 || echo "DNS resolution failed"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: dns_test
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display DNS test result
|
||||||
|
debug:
|
||||||
|
msg: "{{ dns_test.stdout_lines }}"
|
||||||
|
|
||||||
|
- name: Check WireGuard interface status
|
||||||
|
shell: |
|
||||||
|
echo "=== WireGuard Interface Status ==="
|
||||||
|
sudo wg show 2>&1 || echo "WireGuard not running or no permissions"
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: wg_status
|
||||||
|
ignore_errors: yes
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Display WireGuard status
|
||||||
|
debug:
|
||||||
|
msg: "{{ wg_status.stdout_lines }}"
|
||||||
142
deployment/ansible/playbooks/fix-traefik-config.yml
Normal file
142
deployment/ansible/playbooks/fix-traefik-config.yml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
- name: Fix Traefik Configuration
|
||||||
|
hosts: production
|
||||||
|
gather_facts: no
|
||||||
|
become: no
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Backup current traefik.yml
|
||||||
|
shell: |
|
||||||
|
cd ~/deployment/stacks/traefik
|
||||||
|
cp traefik.yml traefik.yml.backup.$(date +%Y%m%d_%H%M%S)
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
|
||||||
|
- name: Create correct traefik.yml
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
# Static Configuration for Traefik
|
||||||
|
|
||||||
|
# Global Configuration
|
||||||
|
global:
|
||||||
|
checkNewVersion: true
|
||||||
|
sendAnonymousUsage: false
|
||||||
|
|
||||||
|
# API and Dashboard
|
||||||
|
# Note: insecure: false means API is only accessible via HTTPS (through Traefik itself)
|
||||||
|
# No port 8080 needed - dashboard accessible via HTTPS at traefik.michaelschiemer.de
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
insecure: false
|
||||||
|
# Dashboard accessible via HTTPS router (no separate HTTP listener needed)
|
||||||
|
|
||||||
|
# Entry Points
|
||||||
|
entryPoints:
|
||||||
|
web:
|
||||||
|
address: ":80"
|
||||||
|
# No global redirect - ACME challenges need HTTP access
|
||||||
|
# Redirects are handled per-router via middleware
|
||||||
|
|
||||||
|
websecure:
|
||||||
|
address: ":443"
|
||||||
|
http:
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
domains:
|
||||||
|
- main: michaelschiemer.de
|
||||||
|
sans:
|
||||||
|
- "*.michaelschiemer.de"
|
||||||
|
|
||||||
|
# Certificate Resolvers
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
email: kontakt@michaelschiemer.de
|
||||||
|
storage: /acme.json
|
||||||
|
caServer: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
# Use HTTP-01 challenge (requires port 80 accessible)
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
# Uncomment for DNS challenge (requires DNS provider)
|
||||||
|
# dnsChallenge:
|
||||||
|
# provider: cloudflare
|
||||||
|
# delayBeforeCheck: 30
|
||||||
|
|
||||||
|
# Providers
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
|
exposedByDefault: false
|
||||||
|
# Network mode is 'host', so we don't specify a network here
|
||||||
|
# Traefik can reach containers directly via their IPs in host network mode
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
file:
|
||||||
|
directory: /dynamic
|
||||||
|
watch: true
|
||||||
|
|
||||||
|
# Forwarded Headers Configuration
|
||||||
|
# This ensures Traefik correctly identifies the real client IP
|
||||||
|
# Important for VPN access where requests come from WireGuard interface
|
||||||
|
forwardedHeaders:
|
||||||
|
trustedIPs:
|
||||||
|
- "10.8.0.0/24" # WireGuard VPN network
|
||||||
|
- "127.0.0.1/32" # Localhost
|
||||||
|
- "172.17.0.0/16" # Docker bridge network
|
||||||
|
- "172.18.0.0/16" # Docker user-defined networks
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
filePath: /logs/traefik.log
|
||||||
|
format: json
|
||||||
|
|
||||||
|
# Access Logs
|
||||||
|
accessLog:
|
||||||
|
filePath: /logs/access.log
|
||||||
|
format: json
|
||||||
|
bufferingSize: 100
|
||||||
|
filters:
|
||||||
|
statusCodes:
|
||||||
|
- "400-499"
|
||||||
|
- "500-599"
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
metrics:
|
||||||
|
prometheus:
|
||||||
|
addEntryPointsLabels: true
|
||||||
|
addRoutersLabels: true
|
||||||
|
addServicesLabels: true
|
||||||
|
|
||||||
|
# Ping
|
||||||
|
ping:
|
||||||
|
entryPoint: web
|
||||||
|
dest: ~/deployment/stacks/traefik/traefik.yml
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Validate YAML syntax
|
||||||
|
command: python3 -c "import yaml; yaml.safe_load(open('traefik.yml')); print('YAML valid')"
|
||||||
|
args:
|
||||||
|
chdir: ~/deployment/stacks/traefik
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Restart Traefik
|
||||||
|
command: docker compose up -d traefik
|
||||||
|
args:
|
||||||
|
chdir: ~/deployment/stacks/traefik
|
||||||
|
register: traefik_restart
|
||||||
|
|
||||||
|
- name: Wait for Traefik to start
|
||||||
|
pause:
|
||||||
|
seconds: 5
|
||||||
|
|
||||||
|
- name: Check Traefik status
|
||||||
|
command: docker compose ps traefik
|
||||||
|
args:
|
||||||
|
chdir: ~/deployment/stacks/traefik
|
||||||
|
register: traefik_status
|
||||||
|
|
||||||
|
- name: Display Traefik status
|
||||||
|
debug:
|
||||||
|
msg: "{{ traefik_status.stdout_lines }}"
|
||||||
@@ -59,17 +59,22 @@
|
|||||||
import_role:
|
import_role:
|
||||||
name: registry
|
name: registry
|
||||||
|
|
||||||
# 4. Deploy MinIO (Object Storage)
|
# 4. Deploy DNS (CoreDNS for WireGuard clients)
|
||||||
|
- name: Deploy DNS stack
|
||||||
|
import_role:
|
||||||
|
name: dns
|
||||||
|
|
||||||
|
# 5. Deploy MinIO (Object Storage)
|
||||||
- name: Deploy MinIO stack
|
- name: Deploy MinIO stack
|
||||||
import_role:
|
import_role:
|
||||||
name: minio
|
name: minio
|
||||||
|
|
||||||
# 5. Deploy Gitea (CRITICAL - Git Server + MySQL + Redis)
|
# 6. Deploy Gitea (CRITICAL - Git Server + MySQL + Redis)
|
||||||
- name: Deploy Gitea stack
|
- name: Deploy Gitea stack
|
||||||
import_role:
|
import_role:
|
||||||
name: gitea
|
name: gitea
|
||||||
|
|
||||||
# 6. Deploy Monitoring (Portainer + Grafana + Prometheus)
|
# 7. Deploy Monitoring (Portainer + Grafana + Prometheus)
|
||||||
- name: Deploy Monitoring stack
|
- name: Deploy Monitoring stack
|
||||||
import_role:
|
import_role:
|
||||||
name: monitoring
|
name: monitoring
|
||||||
@@ -98,7 +103,7 @@
|
|||||||
debug:
|
debug:
|
||||||
msg: "Gitea HTTPS check: {{ 'SUCCESS' if gitea_http_check.status == 200 else 'FAILED - Status: ' + (gitea_http_check.status|string) }}"
|
msg: "Gitea HTTPS check: {{ 'SUCCESS' if gitea_http_check.status == 200 else 'FAILED - Status: ' + (gitea_http_check.status|string) }}"
|
||||||
|
|
||||||
# 7. Deploy Application Stack
|
# 8. Deploy Application Stack
|
||||||
- name: Deploy Application Stack
|
- name: Deploy Application Stack
|
||||||
import_role:
|
import_role:
|
||||||
name: application
|
name: application
|
||||||
@@ -131,6 +136,7 @@
|
|||||||
- "Traefik: {{ 'Deployed' if traefik_stack_changed else 'Already running' }}"
|
- "Traefik: {{ 'Deployed' if traefik_stack_changed else 'Already running' }}"
|
||||||
- "PostgreSQL: {{ 'Deployed' if postgresql_stack_changed else 'Already running' }}"
|
- "PostgreSQL: {{ 'Deployed' if postgresql_stack_changed else 'Already running' }}"
|
||||||
- "Docker Registry: {{ 'Deployed' if registry_stack_changed else 'Already running' }}"
|
- "Docker Registry: {{ 'Deployed' if registry_stack_changed else 'Already running' }}"
|
||||||
|
- "DNS: {{ 'Deployed' if dns_stack_changed else 'Already running' }}"
|
||||||
- "MinIO: {{ 'Deployed' if minio_stack_changed else 'Already running' }}"
|
- "MinIO: {{ 'Deployed' if minio_stack_changed else 'Already running' }}"
|
||||||
- "Gitea: {{ 'Deployed' if gitea_stack_changed else 'Already running' }}"
|
- "Gitea: {{ 'Deployed' if gitea_stack_changed else 'Already running' }}"
|
||||||
- "Monitoring: {{ 'Deployed' if monitoring_stack_changed else 'Already running' }}"
|
- "Monitoring: {{ 'Deployed' if monitoring_stack_changed else 'Already running' }}"
|
||||||
|
|||||||
9
deployment/ansible/roles/dns/defaults/main.yml
Normal file
9
deployment/ansible/roles/dns/defaults/main.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
dns_stack_path: "{{ stacks_base_path }}/dns"
|
||||||
|
dns_corefile_template: "{{ role_path }}/../../templates/dns-Corefile.j2"
|
||||||
|
dns_forwarders:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
dns_records:
|
||||||
|
- host: "grafana.{{ app_domain }}"
|
||||||
|
address: "{{ wireguard_server_ip_default | default('10.8.0.1') }}"
|
||||||
33
deployment/ansible/roles/dns/tasks/main.yml
Normal file
33
deployment/ansible/roles/dns/tasks/main.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
- name: Ensure DNS stack directory exists
|
||||||
|
file:
|
||||||
|
path: "{{ dns_stack_path }}"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
tags:
|
||||||
|
- dns
|
||||||
|
|
||||||
|
- name: Render CoreDNS configuration
|
||||||
|
template:
|
||||||
|
src: "{{ dns_corefile_template }}"
|
||||||
|
dest: "{{ dns_stack_path }}/Corefile"
|
||||||
|
owner: "{{ ansible_user }}"
|
||||||
|
group: "{{ ansible_user }}"
|
||||||
|
mode: '0644'
|
||||||
|
tags:
|
||||||
|
- dns
|
||||||
|
|
||||||
|
- name: Deploy DNS stack
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ dns_stack_path }}"
|
||||||
|
state: present
|
||||||
|
pull: always
|
||||||
|
register: dns_compose_result
|
||||||
|
tags:
|
||||||
|
- dns
|
||||||
|
|
||||||
|
- name: Record DNS deployment facts
|
||||||
|
set_fact:
|
||||||
|
dns_stack_changed: "{{ dns_compose_result.changed | default(false) }}"
|
||||||
|
tags:
|
||||||
|
- dns
|
||||||
@@ -3,4 +3,5 @@ monitoring_stack_path: "{{ stacks_base_path }}/monitoring"
|
|||||||
monitoring_wait_timeout: "{{ wait_timeout | default(60) }}"
|
monitoring_wait_timeout: "{{ wait_timeout | default(60) }}"
|
||||||
monitoring_env_template: "{{ role_path }}/../../templates/monitoring.env.j2"
|
monitoring_env_template: "{{ role_path }}/../../templates/monitoring.env.j2"
|
||||||
monitoring_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
|
monitoring_vault_file: "{{ role_path }}/../../secrets/production.vault.yml"
|
||||||
monitoring_vpn_ip_whitelist: "{{ wireguard_network_default | default('10.8.0.0/24') }}"
|
# VPN IP whitelist: Allow WireGuard VPN network only (override via extra vars if needed)
|
||||||
|
monitoring_vpn_ip_whitelist: "{{ monitoring_vpn_ip_whitelist_ranges | default([wireguard_network_default | default('10.8.0.0/24')]) | join(',') }}"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
no_log: yes
|
no_log: yes
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
become: no
|
become: no
|
||||||
|
ignore_errors: yes
|
||||||
tags:
|
tags:
|
||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
@@ -48,6 +49,36 @@
|
|||||||
tags:
|
tags:
|
||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
|
- name: Build VPN IP whitelist with endpoints
|
||||||
|
set_fact:
|
||||||
|
monitoring_vpn_ip_whitelist_ranges: "{{ [wireguard_network_default | default('10.8.0.0/24')] }}"
|
||||||
|
tags:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
- name: Set VPN IP whitelist for monitoring
|
||||||
|
set_fact:
|
||||||
|
monitoring_vpn_ip_whitelist: "{{ monitoring_vpn_ip_whitelist_ranges | join(',') }}"
|
||||||
|
tags:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
- name: Set Traefik stack path
|
||||||
|
set_fact:
|
||||||
|
traefik_stack_path: "{{ stacks_base_path }}/traefik"
|
||||||
|
tags:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
- name: Update Traefik middleware with dynamic VPN IPs
|
||||||
|
template:
|
||||||
|
src: "{{ role_path }}/../../templates/traefik-middlewares.yml.j2"
|
||||||
|
dest: "{{ traefik_stack_path }}/dynamic/middlewares.yml"
|
||||||
|
owner: "{{ ansible_user }}"
|
||||||
|
group: "{{ ansible_user }}"
|
||||||
|
mode: '0644'
|
||||||
|
vars:
|
||||||
|
vpn_network: "{{ wireguard_network_default | default('10.8.0.0/24') }}"
|
||||||
|
tags:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
- name: Ensure monitoring stack directory exists
|
- name: Ensure monitoring stack directory exists
|
||||||
file:
|
file:
|
||||||
path: "{{ monitoring_stack_path }}"
|
path: "{{ monitoring_stack_path }}"
|
||||||
|
|||||||
15
deployment/ansible/templates/dns-Corefile.j2
Normal file
15
deployment/ansible/templates/dns-Corefile.j2
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
. {
|
||||||
|
log
|
||||||
|
errors
|
||||||
|
health :8053
|
||||||
|
hosts {
|
||||||
|
{% for record in dns_records %}
|
||||||
|
{{ record.address }} {{ record.host }}
|
||||||
|
{% endfor %}
|
||||||
|
fallthrough
|
||||||
|
}
|
||||||
|
{% if dns_forwarders | length > 0 %}
|
||||||
|
forward . {{ dns_forwarders | join(' ') }}
|
||||||
|
{% endif %}
|
||||||
|
cache 30
|
||||||
|
}
|
||||||
82
deployment/ansible/templates/traefik-middlewares.yml.j2
Normal file
82
deployment/ansible/templates/traefik-middlewares.yml.j2
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Dynamic Middleware Configuration
|
||||||
|
|
||||||
|
http:
|
||||||
|
middlewares:
|
||||||
|
# Security headers for all services
|
||||||
|
security-headers-global:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
browserXssFilter: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
customFrameOptionsValue: "SAMEORIGIN"
|
||||||
|
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
|
||||||
|
referrerPolicy: "strict-origin-when-cross-origin"
|
||||||
|
permissionsPolicy: "geolocation=(), microphone=(), camera=()"
|
||||||
|
|
||||||
|
# Compression for better performance
|
||||||
|
gzip-compression:
|
||||||
|
compress:
|
||||||
|
excludedContentTypes:
|
||||||
|
- text/event-stream
|
||||||
|
|
||||||
|
# Rate limiting - strict
|
||||||
|
rate-limit-strict:
|
||||||
|
rateLimit:
|
||||||
|
average: 50
|
||||||
|
burst: 25
|
||||||
|
period: 1s
|
||||||
|
|
||||||
|
# Rate limiting - moderate
|
||||||
|
rate-limit-moderate:
|
||||||
|
rateLimit:
|
||||||
|
average: 100
|
||||||
|
burst: 50
|
||||||
|
period: 1s
|
||||||
|
|
||||||
|
# Rate limiting - lenient
|
||||||
|
rate-limit-lenient:
|
||||||
|
rateLimit:
|
||||||
|
average: 200
|
||||||
|
burst: 100
|
||||||
|
period: 1s
|
||||||
|
|
||||||
|
# IP whitelist for admin services (example)
|
||||||
|
# Uncomment and adjust for production
|
||||||
|
# admin-whitelist:
|
||||||
|
# ipWhiteList:
|
||||||
|
# sourceRange:
|
||||||
|
# - "127.0.0.1/32"
|
||||||
|
# - "10.0.0.0/8"
|
||||||
|
|
||||||
|
# VPN-only IP whitelist for Grafana and other monitoring services
|
||||||
|
# Restrict access strictly to the WireGuard VPN network
|
||||||
|
grafana-vpn-only:
|
||||||
|
ipWhiteList:
|
||||||
|
sourceRange:
|
||||||
|
- "{{ vpn_network }}" # WireGuard VPN network
|
||||||
|
|
||||||
|
# VPN-only IP whitelist for general use (Traefik Dashboard, etc.)
|
||||||
|
# Restrict access strictly to the WireGuard network
|
||||||
|
vpn-only:
|
||||||
|
ipWhiteList:
|
||||||
|
sourceRange:
|
||||||
|
- "{{ vpn_network }}" # WireGuard VPN network
|
||||||
|
|
||||||
|
# Chain multiple middlewares
|
||||||
|
default-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- security-headers-global
|
||||||
|
- gzip-compression
|
||||||
|
|
||||||
|
admin-chain:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- security-headers-global
|
||||||
|
- gzip-compression
|
||||||
|
- rate-limit-strict
|
||||||
|
# - admin-whitelist # Uncomment for IP whitelisting
|
||||||
@@ -8,8 +8,8 @@ PrivateKey = {{ client_private_key.stdout }}
|
|||||||
# Client IP address in VPN network
|
# Client IP address in VPN network
|
||||||
Address = {{ client_ip }}/24
|
Address = {{ client_ip }}/24
|
||||||
|
|
||||||
# DNS server (optional)
|
# DNS server (VPN internal resolver)
|
||||||
DNS = 1.1.1.1, 8.8.8.8
|
DNS = {{ wireguard_dns_servers | join(', ') }}
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
# Server public key
|
# Server public key
|
||||||
@@ -24,4 +24,4 @@ Endpoint = {{ server_external_ip_content }}:{{ wireguard_port }}
|
|||||||
AllowedIPs = {{ allowed_ips }}
|
AllowedIPs = {{ allowed_ips }}
|
||||||
|
|
||||||
# Keep connection alive
|
# Keep connection alive
|
||||||
PersistentKeepalive = 25
|
PersistentKeepalive = 25
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
[Interface]
|
[Interface]
|
||||||
# Client private key
|
# Client private key
|
||||||
PrivateKey = uMhNKh+Wi0aykTnazfSJD6l7Wc2V1Pe+7rFtFcnfynw=
|
PrivateKey = sE81MBr64fP8YBDlhRWngwHHmlrVzIhs9NT7Dh7XbVs=
|
||||||
|
|
||||||
# Client IP address in VPN network
|
# Client IP address in VPN network
|
||||||
Address = 10.8.0.4/24
|
Address = 10.8.0.7/24
|
||||||
|
|
||||||
# DNS server (optional)
|
# DNS server (VPN internal resolver)
|
||||||
DNS = 1.1.1.1, 8.8.8.8
|
DNS = 10.8.0.1
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
# Server public key
|
# Server public key
|
||||||
@@ -24,4 +24,4 @@ Endpoint = 94.16.110.151:51820
|
|||||||
AllowedIPs = 10.8.0.0/24
|
AllowedIPs = 10.8.0.0/24
|
||||||
|
|
||||||
# Keep connection alive
|
# Keep connection alive
|
||||||
PersistentKeepalive = 25
|
PersistentKeepalive = 25
|
||||||
|
|||||||
14
deployment/stacks/dns/docker-compose.yml
Normal file
14
deployment/stacks/dns/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
coredns:
|
||||||
|
image: coredns/coredns:1.11.1
|
||||||
|
container_name: coredns
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
command: -conf /etc/coredns/Corefile
|
||||||
|
volumes:
|
||||||
|
- ./Corefile:/etc/coredns/Corefile:ro
|
||||||
|
healthcheck:
|
||||||
|
# Disable healthcheck - CoreDNS is a minimal image without shell
|
||||||
|
# CoreDNS runs fine (verified by DNS queries working correctly)
|
||||||
|
# If needed, health can be checked externally via dig
|
||||||
|
disable: true
|
||||||
@@ -75,8 +75,9 @@ services:
|
|||||||
- "traefik.http.routers.grafana.entrypoints=websecure"
|
- "traefik.http.routers.grafana.entrypoints=websecure"
|
||||||
- "traefik.http.routers.grafana.tls=true"
|
- "traefik.http.routers.grafana.tls=true"
|
||||||
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.middlewares.grafana-vpn-only.ipwhitelist.sourcerange=${MONITORING_VPN_IP_WHITELIST:-10.8.0.0/24}"
|
# VPN IP whitelist: Use middleware defined in Traefik dynamic config
|
||||||
- "traefik.http.routers.grafana.middlewares=grafana-vpn-only"
|
# Middleware is defined in deployment/stacks/traefik/dynamic/middlewares.yml
|
||||||
|
- "traefik.http.routers.grafana.middlewares=grafana-vpn-only@file"
|
||||||
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
|
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
prometheus:
|
prometheus:
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ Traefik acts as the central reverse proxy for all services, handling:
|
|||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
- **traefik.michaelschiemer.de** - Traefik Dashboard (BasicAuth protected)
|
- **traefik.michaelschiemer.de** - Traefik Dashboard (VPN-only + BasicAuth protected)
|
||||||
|
- ?? **Nur ?ber WireGuard VPN erreichbar** (10.8.0.0/24)
|
||||||
|
- Zus?tzlich durch BasicAuth gesch?tzt
|
||||||
|
- ?ffentlicher Zugriff ist blockiert
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -126,6 +129,16 @@ labels:
|
|||||||
- "traefik.http.routers.myapp.middlewares=gzip-compression@file"
|
- "traefik.http.routers.myapp.middlewares=gzip-compression@file"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### VPN-Only Access (WireGuard Network)
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
# Restrict access to WireGuard VPN network only (10.8.0.0/24)
|
||||||
|
- "traefik.http.routers.myapp.middlewares=vpn-only@file"
|
||||||
|
|
||||||
|
# Combined: VPN-only + BasicAuth (order matters - VPN check first, then BasicAuth)
|
||||||
|
- "traefik.http.routers.myapp.middlewares=vpn-only@file,traefik-auth"
|
||||||
|
```
|
||||||
|
|
||||||
### Middleware Chains
|
### Middleware Chains
|
||||||
```yaml
|
```yaml
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
networks:
|
# Use host network mode to correctly identify client IPs from WireGuard
|
||||||
- traefik-public
|
# Without this, Traefik sees Docker bridge IPs instead of real client IPs (10.8.0.x)
|
||||||
ports:
|
network_mode: host
|
||||||
- "80:80"
|
# When using host network mode, we don't bind ports in docker-compose
|
||||||
- "443:443"
|
# Traefik listens directly on host ports 80 and 443
|
||||||
|
# ports:
|
||||||
|
# - "80:80"
|
||||||
|
# - "443:443"
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
volumes:
|
volumes:
|
||||||
@@ -27,13 +30,15 @@ services:
|
|||||||
# Enable Traefik for itself
|
# Enable Traefik for itself
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
|
|
||||||
# Dashboard
|
# Dashboard - VPN-only access (WireGuard network required)
|
||||||
|
# Accessible only from WireGuard VPN network (10.8.0.0/24)
|
||||||
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.michaelschiemer.de`)"
|
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.michaelschiemer.de`)"
|
||||||
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
|
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
|
||||||
- "traefik.http.routers.traefik-dashboard.tls=true"
|
- "traefik.http.routers.traefik-dashboard.tls=true"
|
||||||
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.routers.traefik-dashboard.service=api@internal"
|
- "traefik.http.routers.traefik-dashboard.service=api@internal"
|
||||||
- "traefik.http.routers.traefik-dashboard.middlewares=traefik-auth"
|
# VPN-only + BasicAuth protection (order: vpn-only first, then BasicAuth)
|
||||||
|
- "traefik.http.routers.traefik-dashboard.middlewares=vpn-only@file,traefik-auth"
|
||||||
|
|
||||||
# BasicAuth for dashboard (user: admin, password: generate with htpasswd)
|
# BasicAuth for dashboard (user: admin, password: generate with htpasswd)
|
||||||
# htpasswd -nb admin your_password
|
# htpasswd -nb admin your_password
|
||||||
@@ -73,6 +78,5 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
networks:
|
# Note: network_mode: host is used, so we don't define networks here
|
||||||
traefik-public:
|
# Traefik still discovers services via Docker labels using the Docker socket
|
||||||
external: true
|
|
||||||
|
|||||||
@@ -51,6 +51,20 @@ http:
|
|||||||
# sourceRange:
|
# sourceRange:
|
||||||
# - "127.0.0.1/32"
|
# - "127.0.0.1/32"
|
||||||
# - "10.0.0.0/8"
|
# - "10.0.0.0/8"
|
||||||
|
|
||||||
|
# VPN-only IP whitelist for Grafana and other monitoring services
|
||||||
|
# Restrict access strictly to the WireGuard network
|
||||||
|
grafana-vpn-only:
|
||||||
|
ipWhiteList:
|
||||||
|
sourceRange:
|
||||||
|
- "10.8.0.0/24" # WireGuard VPN network
|
||||||
|
|
||||||
|
# VPN-only IP whitelist for general use (Traefik Dashboard, etc.)
|
||||||
|
# Restrict access strictly to the WireGuard network
|
||||||
|
vpn-only:
|
||||||
|
ipWhiteList:
|
||||||
|
sourceRange:
|
||||||
|
- "10.8.0.0/24" # WireGuard VPN network
|
||||||
|
|
||||||
# Chain multiple middlewares
|
# Chain multiple middlewares
|
||||||
default-chain:
|
default-chain:
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ global:
|
|||||||
sendAnonymousUsage: false
|
sendAnonymousUsage: false
|
||||||
|
|
||||||
# API and Dashboard
|
# API and Dashboard
|
||||||
|
# Note: insecure: false means API is only accessible via HTTPS (through Traefik itself)
|
||||||
|
# No port 8080 needed - dashboard accessible via HTTPS at traefik.michaelschiemer.de
|
||||||
api:
|
api:
|
||||||
dashboard: true
|
dashboard: true
|
||||||
insecure: false
|
insecure: false
|
||||||
|
# Dashboard accessible via HTTPS router (no separate HTTP listener needed)
|
||||||
|
|
||||||
# Entry Points
|
# Entry Points
|
||||||
entryPoints:
|
entryPoints:
|
||||||
@@ -26,9 +29,6 @@ entryPoints:
|
|||||||
- main: michaelschiemer.de
|
- main: michaelschiemer.de
|
||||||
sans:
|
sans:
|
||||||
- "*.michaelschiemer.de"
|
- "*.michaelschiemer.de"
|
||||||
middlewares:
|
|
||||||
- security-headers@docker
|
|
||||||
- compression@docker
|
|
||||||
|
|
||||||
# Certificate Resolvers
|
# Certificate Resolvers
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
@@ -50,13 +50,25 @@ providers:
|
|||||||
docker:
|
docker:
|
||||||
endpoint: "unix:///var/run/docker.sock"
|
endpoint: "unix:///var/run/docker.sock"
|
||||||
exposedByDefault: false
|
exposedByDefault: false
|
||||||
network: traefik-public
|
# Network mode is 'host', so we don't specify a network here
|
||||||
|
# Traefik can reach containers directly via their IPs in host network mode
|
||||||
watch: true
|
watch: true
|
||||||
|
|
||||||
file:
|
file:
|
||||||
directory: /dynamic
|
directory: /dynamic
|
||||||
watch: true
|
watch: true
|
||||||
|
|
||||||
|
# Forwarded Headers Configuration
|
||||||
|
# This ensures Traefik correctly identifies the real client IP
|
||||||
|
# Important for VPN access where requests come from WireGuard interface
|
||||||
|
forwardedHeaders:
|
||||||
|
trustedIPs:
|
||||||
|
- "10.8.0.0/24" # WireGuard VPN network
|
||||||
|
- "127.0.0.1/32" # Localhost
|
||||||
|
- "172.17.0.0/16" # Docker bridge network
|
||||||
|
- "172.18.0.0/16" # Docker user-defined networks
|
||||||
|
insecure: false
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
log:
|
log:
|
||||||
level: INFO
|
level: INFO
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ Dieses Dokument beschreibt die geplante Sicherheitshärtung für Backend-Dienste
|
|||||||
|
|
||||||
**Aktueller Zustand:**
|
**Aktueller Zustand:**
|
||||||
- ✅ WireGuard VPN ist installiert und funktionsfähig
|
- ✅ WireGuard VPN ist installiert und funktionsfähig
|
||||||
- ✅ Backend-Dienste sind öffentlich über Traefik erreichbar
|
- ✅ Traefik-Dashboard ist jetzt VPN-only (nur über WireGuard erreichbar)
|
||||||
|
- ✅ Grafana ist VPN-only (nur über WireGuard erreichbar)
|
||||||
|
- ✅ Andere Backend-Dienste sind noch öffentlich über Traefik erreichbar
|
||||||
- ✅ VPN-Zugriff funktioniert parallel
|
- ✅ VPN-Zugriff funktioniert parallel
|
||||||
|
|
||||||
**Zielzustand:**
|
**Zielzustand:**
|
||||||
@@ -38,11 +40,19 @@ Folgende Dienste sollen später nur noch über VPN erreichbar sein:
|
|||||||
- Aktuell: `https://portainer.michaelschiemer.de` (öffentlich)
|
- Aktuell: `https://portainer.michaelschiemer.de` (öffentlich)
|
||||||
- Zukünftig: Nur `https://10.8.0.1:9443` über VPN
|
- Zukünftig: Nur `https://10.8.0.1:9443` über VPN
|
||||||
|
|
||||||
## Aktueller Test: Grafana nur per VPN
|
## Aktuell implementiert: VPN-only Services
|
||||||
|
|
||||||
- Realisiert über Traefik-IP-Whitelist (`grafana-vpn-only` Middleware)
|
### Traefik-Dashboard
|
||||||
- Whitelist-Wert: `MONITORING_VPN_IP_WHITELIST` in `deployment/stacks/monitoring/.env` (Ansible default `monitoring_vpn_ip_whitelist`)
|
- ✅ Realisiert über Traefik-IP-Whitelist (`vpn-only` Middleware)
|
||||||
- Ziel: Erst Grafana absichern, später Prometheus/Portainer nachziehen
|
- ✅ Zusätzlich durch BasicAuth geschützt
|
||||||
|
- ✅ Whitelist umfasst standardmäßig ausschließlich das WireGuard-Netz `10.8.0.0/24`
|
||||||
|
- ✅ Öffentlicher Zugriff ist blockiert (403 Forbidden)
|
||||||
|
|
||||||
|
### Grafana
|
||||||
|
- ✅ Realisiert über Traefik-IP-Whitelist (`grafana-vpn-only` Middleware)
|
||||||
|
- ✅ Whitelist umfasst standardmäßig ausschließlich das WireGuard-Netz `10.8.0.0/24`
|
||||||
|
- ✅ Anpassung via Ansible-Extra-Var `monitoring_vpn_ip_whitelist_ranges` möglich (falls weitere Netze freigeschaltet werden sollen)
|
||||||
|
- ✅ CoreDNS-Override aktiv, damit `grafana.michaelschiemer.de` im VPN auf `10.8.0.1` zeigt
|
||||||
|
|
||||||
### Rollout-Schritte
|
### Rollout-Schritte
|
||||||
1. Neue Stacks auf den Server syncen: \
|
1. Neue Stacks auf den Server syncen: \
|
||||||
@@ -55,6 +65,8 @@ Folgende Dienste sollen später nur noch über VPN erreichbar sein:
|
|||||||
- Ohne VPN (öffentliche IP): `curl -I https://grafana.michaelschiemer.de` → `403 Forbidden`
|
- Ohne VPN (öffentliche IP): `curl -I https://grafana.michaelschiemer.de` → `403 Forbidden`
|
||||||
- Mit WireGuard (`10.8.0.x` Client): Grafana im Browser laden, Login funktioniert
|
- Mit WireGuard (`10.8.0.x` Client): Grafana im Browser laden, Login funktioniert
|
||||||
- Optional: Traefik-Logs prüfen (`docker compose -f ~/deployment/stacks/traefik/docker-compose.yml logs -f traefik`) für geblockte IPs
|
- Optional: Traefik-Logs prüfen (`docker compose -f ~/deployment/stacks/traefik/docker-compose.yml logs -f traefik`) für geblockte IPs
|
||||||
|
- DNS-Check im VPN: `dig grafana.michaelschiemer.de @10.8.0.1` → `10.8.0.1`
|
||||||
|
- WireGuard-Client bekommt automatisch `DNS = 10.8.0.1`, solange `wireguard_dns_servers` nicht überschrieben wird (ansonsten manuell auf internen Resolver setzen)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,15 @@ hT3OCWZ6ElX79YdAdexSsZnbWLzRM/5szk+XNEBUaS8=
|
|||||||
- ✅ Normale Internet-Routen werden nicht geändert
|
- ✅ Normale Internet-Routen werden nicht geändert
|
||||||
- ✅ Firewall-Regeln für SSH (Port 22) werden NICHT entfernt oder blockiert
|
- ✅ Firewall-Regeln für SSH (Port 22) werden NICHT entfernt oder blockiert
|
||||||
|
|
||||||
|
**Aktuelle Sicherheitshärtung:**
|
||||||
|
- 🔒 **Traefik-Dashboard** ist jetzt nur noch über WireGuard VPN erreichbar (VPN-only + BasicAuth)
|
||||||
|
- 🔒 **Grafana** ist nur noch über WireGuard VPN erreichbar (VPN-only Middleware)
|
||||||
|
- ✅ Hauptanwendung (`michaelschiemer.de`) bleibt öffentlich erreichbar (wie gewünscht)
|
||||||
|
- 🔒 Traefik arbeitet hinter WireGuard für interne Services (Dashboard, Monitoring)
|
||||||
|
|
||||||
**Zukünftige Sicherheitshärtung (geplant):**
|
**Zukünftige Sicherheitshärtung (geplant):**
|
||||||
- 🔒 Backend-Dienste (Prometheus, Grafana, Portainer) sollen später nur noch über VPN erreichbar sein
|
- 🔒 Weitere Backend-Dienste (Prometheus, Portainer) können später nur noch über VPN erreichbar sein
|
||||||
- 🔒 Aktuell sind alle Dienste noch öffentlich erreichbar (für einfachere Einrichtung)
|
- 🔒 Firewall-Regeln können bei Bedarf weiter angepasst werden
|
||||||
- 🔒 Firewall-Regeln können später angepasst werden, um nur VPN-Zugriff zu erlauben
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -485,6 +490,7 @@ ansible-playbook -i inventory/production.yml playbooks/add-wireguard-client.yml
|
|||||||
- `client_name`: Name des Clients (erforderlich)
|
- `client_name`: Name des Clients (erforderlich)
|
||||||
- `client_ip`: Spezifische Client-IP (Standard: automatisch berechnet)
|
- `client_ip`: Spezifische Client-IP (Standard: automatisch berechnet)
|
||||||
- `allowed_ips`: Erlaubte IP-Ranges (Standard: 10.8.0.0/24)
|
- `allowed_ips`: Erlaubte IP-Ranges (Standard: 10.8.0.0/24)
|
||||||
|
- `wireguard_dns_servers`: Liste interner Resolver (Default: WireGuard-Server-IP, z.B. `["10.8.0.1"]`)
|
||||||
|
|
||||||
**Dokumentation**: Siehe `deployment/ansible/playbooks/README-WIREGUARD.md`
|
**Dokumentation**: Siehe `deployment/ansible/playbooks/README-WIREGUARD.md`
|
||||||
|
|
||||||
@@ -492,6 +498,33 @@ ansible-playbook -i inventory/production.yml playbooks/add-wireguard-client.yml
|
|||||||
> `deployment/ansible/wireguard-clients/<client_name>.conf` – ideal zum direkten
|
> `deployment/ansible/wireguard-clients/<client_name>.conf` – ideal zum direkten
|
||||||
> Import auf deinem Admin-Rechner (Datei bleibt mit `chmod 600` geschützt).
|
> Import auf deinem Admin-Rechner (Datei bleibt mit `chmod 600` geschützt).
|
||||||
|
|
||||||
|
### Interner DNS (CoreDNS) für VPN-Clients
|
||||||
|
|
||||||
|
**Datei**: `deployment/ansible/playbooks/setup-infrastructure.yml` (Tag `dns`)
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
```bash
|
||||||
|
cd deployment/ansible
|
||||||
|
ANSIBLE_VAULT_PASSWORD_FILE=secrets/.vault_pass \
|
||||||
|
ansible-playbook -i inventory/production.yml playbooks/setup-infrastructure.yml --tags dns
|
||||||
|
```
|
||||||
|
|
||||||
|
**Konfiguration:**
|
||||||
|
- DNS-Records definieren sich über `dns_records` (Default in `group_vars/production.yml` → `grafana.<app_domain>` → `10.8.0.1`)
|
||||||
|
- Zusätzliche Einträge können per Override vor dem Playbook-Lauf gesetzt werden, z.B.:
|
||||||
|
```bash
|
||||||
|
ansible-playbook ... --tags dns \
|
||||||
|
-e 'dns_records=[{"host":"grafana.michaelschiemer.de","address":"10.8.0.1"},{"host":"prometheus.michaelschiemer.de","address":"10.8.0.1"}]'
|
||||||
|
```
|
||||||
|
- Upstream-Resolver werden über `dns_forwarders` gesteuert (Standard: `1.1.1.1`, `8.8.8.8`)
|
||||||
|
|
||||||
|
**Ergebnis:**
|
||||||
|
- CoreDNS läuft auf dem Server via `network_mode: host` (UDP/TCP 53)
|
||||||
|
- Alle VPN-Clients können `grafana.michaelschiemer.de` (und weitere Overrides) direkt auf `10.8.0.1` auflösen
|
||||||
|
- Nicht hinterlegte Hostnamen werden dank `fallthrough` automatisch an die definierten Upstream-Resolver weitergereicht
|
||||||
|
- Traefik-Middleware `grafana-vpn-only` akzeptiert ausschließlich Verbindungen aus dem WireGuard-Netz `10.8.0.0/24`
|
||||||
|
- HTTP-Aufrufe zu Grafana laufen so automatisch durch den WireGuard-Tunnel
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Verzeichnisstruktur
|
## Verzeichnisstruktur
|
||||||
|
|||||||
23
src/Framework/Core/System/Ini/Access.php
Normal file
23
src/Framework/Core/System/Ini/Access.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Core\System\Ini;
|
||||||
|
|
||||||
|
enum Access: string
|
||||||
|
{
|
||||||
|
case USER = "USER";
|
||||||
|
case PERDIR = "Per Directory";
|
||||||
|
case SYSTEM = "System";
|
||||||
|
case ALL = "All";
|
||||||
|
|
||||||
|
public static function fromBitmask(int $bitmask): self
|
||||||
|
{
|
||||||
|
return match ($bitmask) {
|
||||||
|
INI_USER => self::USER,
|
||||||
|
INI_PERDIR => self::PERDIR,
|
||||||
|
INI_SYSTEM => self::SYSTEM,
|
||||||
|
INI_ALL => self::ALL,
|
||||||
|
default => throw new \InvalidArgumentException("Invalid bitmask value: {$bitmask}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Framework/Core/System/Ini/IniDirective.php
Normal file
20
src/Framework/Core/System/Ini/IniDirective.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Core\System\Ini;
|
||||||
|
|
||||||
|
final class IniDirective
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $name,
|
||||||
|
public string $value,
|
||||||
|
public string $global,
|
||||||
|
private int $accessMask,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getAccess(): int
|
||||||
|
{
|
||||||
|
$access = Access::fromBitmask($this->accessMask);
|
||||||
|
return $this->accessMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/Framework/Core/System/Ini/IniKey.php
Normal file
62
src/Framework/Core/System/Ini/IniKey.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Core\System\Ini;
|
||||||
|
|
||||||
|
enum IniKey: string
|
||||||
|
{
|
||||||
|
case ALLOW_URL_INCLUDE = "allow_url_include";
|
||||||
|
case ALLOW_URL_FOPEN = "allow_url_fopen";
|
||||||
|
case ALLOW_URL_FOPEN_UPLOAD = "allow_url_fopen_upload";
|
||||||
|
case ALLOW_URL_STREAM = "allow_url_stream";
|
||||||
|
case ALLOW_URL_STREAM_WRAPPER = "allow_url_stream_wrapper";
|
||||||
|
case ALLOW_URL_WRAPPER = "allow_url_wrapper";
|
||||||
|
case DEFAULT_SOCKET_TIMEOUT = "default_socket_timeout";
|
||||||
|
case DISABLE_FUNCTIONS = "disable_functions";
|
||||||
|
case DISABLE_CLASSES = "disable_classes";
|
||||||
|
case DISABLE_CLASSES_REFLECTION = "disable_classes_reflection";
|
||||||
|
case DISABLE_CONSTANTS = "disable_constants";
|
||||||
|
case DISABLE_ERRORS = "disable_errors";
|
||||||
|
case DISABLE_INCLUDE_PATH = "disable_include_path";
|
||||||
|
case DISABLE_PATH_INJECTION = "disable_path_injection";
|
||||||
|
case DISABLE_PHP = "disable_php";
|
||||||
|
case DISABLE_REFLECTION = "disable_reflection";
|
||||||
|
case ENABLE_DL = "enable_dl";
|
||||||
|
case ENABLE_POST_DATA_BEING_SENT = "enable_post_data_being_sent";
|
||||||
|
case ENABLE_SESSION = "enable_session";
|
||||||
|
case ERROR_REPORTING = "error_reporting";
|
||||||
|
case HTML_ERRORS = "html_errors";
|
||||||
|
case HTML_ERRORS_404 = "html_errors_404";
|
||||||
|
case HTML_ERRORS_404_LOG = "html_errors_404_log";
|
||||||
|
case HTML_ERRORS_404_SKIP = "html_errors_404_skip";
|
||||||
|
case HTML_ERRORS_404_TITLE = "html_errors_404_title";
|
||||||
|
case HTML_ERRORS_500 = "html_errors_500";
|
||||||
|
case HTML_ERRORS_500_LOG = "html_errors_500_log";
|
||||||
|
case HTML_ERRORS_500_SKIP = "html_errors_500_skip";
|
||||||
|
case HTML_ERRORS_500_TITLE = "html_errors_500_title";
|
||||||
|
case HTML_ERRORS_LOG = "html_errors_log";
|
||||||
|
case HTML_ERRORS_SKIP = "html_errors_skip";
|
||||||
|
case HTML_ERRORS_TITLE = "html_errors_title";
|
||||||
|
case HTML_ERRORS_TYPE = "html_errors_type";
|
||||||
|
case HTML_ERRORS_USE_INCLUDE_PATH = "html_errors_use_include_path";
|
||||||
|
case HTML_SAFE_EMAILS = "html_safe_emails";
|
||||||
|
case HTML_SAFE_URLS = "html_safe_urls";
|
||||||
|
case IGNORE_REPEATED_ERRORS = "ignore_repeated_errors";
|
||||||
|
case IGNORE_REPEATED_SOURCE = "ignore_repeated_source";
|
||||||
|
case IGNORE_USER_ABORT = "ignore_user_abort";
|
||||||
|
case LOG_ERRORS = "log_errors";
|
||||||
|
case LOG_ERRORS_MAX_LEN = "log_errors_max_len";
|
||||||
|
case LOG_ERRORS_MSG = "log_errors_msg";
|
||||||
|
case LOG_ERRORS_TO_STDOUT = "log_errors_to_stdout";
|
||||||
|
case LOG_ERRORS_USE_INCLUDE_PATH = "log_errors_use_include_path";
|
||||||
|
case MEMORY_LIMIT = "memory_limit";
|
||||||
|
case OPCACHE_ENABLE = "opcache.enable";
|
||||||
|
case OPCACHE_ENABLE_CLI = "opcache.enable_cli";
|
||||||
|
case OPCACHE_ENABLE_FILE_OVERRIDE = "opcache.enable_file_override";
|
||||||
|
case OPCACHE_ENABLE_FILE_OVERRIDE_IF_EXISTS = "opcache.enable_file_override_if_exists";
|
||||||
|
case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX = "opcache.enable_file_override_from_index";
|
||||||
|
case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX_IF_EXISTS = "opcache.enable_file_override_from_index_if_exists";
|
||||||
|
case OPCACHE_ENABLE_FILE_OVERRIDE_FROM_INDEX_IF_EXISTS_IF_EMPTY = "opcache.enable_file_override_from_index_if_exists_if_empty";
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
30
src/Framework/Core/System/PhpIni.php
Normal file
30
src/Framework/Core/System/PhpIni.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Core\System;
|
||||||
|
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
final readonly class PhpIni implements Stringable
|
||||||
|
{
|
||||||
|
public string $path;
|
||||||
|
public function __construct(
|
||||||
|
) {
|
||||||
|
$path = php_ini_loaded_file();
|
||||||
|
if($path === false) {
|
||||||
|
$path = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->path = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLoaded(): bool
|
||||||
|
{
|
||||||
|
return $this->path !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -453,7 +453,7 @@ final readonly class FilePath implements Stringable
|
|||||||
|
|
||||||
// Check for suspicious patterns (basic path traversal)
|
// Check for suspicious patterns (basic path traversal)
|
||||||
if (str_contains($path, '..')) {
|
if (str_contains($path, '..')) {
|
||||||
// Allow .. in normalized paths, but check final result doesn't escape intended boundaries
|
// Allow .. in normalized paths, but check that the final result doesn't escape intended boundaries
|
||||||
// This is a basic check - more sophisticated validation can be added
|
// This is a basic check - more sophisticated validation can be added
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ final class RedisConnectionPool
|
|||||||
*/
|
*/
|
||||||
public function registerConnection(string $name, RedisConfig $config): void
|
public function registerConnection(string $name, RedisConfig $config): void
|
||||||
{
|
{
|
||||||
|
var_dump("<pre>", $config);
|
||||||
$this->configs[$name] = $config;
|
$this->configs[$name] = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user