From 9e7b2b3b844da2183ee440b3f7d1138742aae1bd Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 7 Nov 2025 11:52:41 +0100 Subject: [PATCH] chore: upgrade reverseproxy role for use with vagrant and ssl --- roles/reverseproxy/defaults/main.yml | 65 +++++++++++++++++-- roles/reverseproxy/meta/main.yml | 5 +- roles/reverseproxy/tasks/main.yml | 58 ++++++++++------- .../templates/docker-compose.yml.j2 | 32 +++++++-- .../reverseproxy/templates/middlewares.yml.j2 | 58 ++++------------- roles/reverseproxy/templates/services.yml.j2 | 29 +++++---- roles/reverseproxy/templates/traefik.yml.j2 | 34 ++++++++-- 7 files changed, 177 insertions(+), 104 deletions(-) diff --git a/roles/reverseproxy/defaults/main.yml b/roles/reverseproxy/defaults/main.yml index a1d5300..7e3743d 100644 --- a/roles/reverseproxy/defaults/main.yml +++ b/roles/reverseproxy/defaults/main.yml @@ -2,13 +2,68 @@ --- # defaults file for reverseproxy +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose +docker_volume_base_dir: /srv/data + # Service-specific configuration service_name: reverseproxy docker_compose_dir: "{{ docker_compose_base_dir }}/{{ service_name }}" docker_volume_dir: "{{ docker_volume_base_dir }}/{{ service_name }}" -# Provider configuration -use_static_services: false # Use all_services from services.yml for outward-facing proxies -use_docker_provider: true # Use Docker provider for service discovery via labels -use_ssl: false # Enable SSL termination with Let's Encrypt -enable_dashboard: true # Enable Traefik dashboard \ No newline at end of file +# Deployment mode: 'dmz' or 'backend' +# - dmz: Public-facing reverse proxy that routes to backend servers using file provider +# - backend: Application server with docker provider for local container discovery +reverseproxy_mode: "backend" + +# SSL configuration +use_ssl: true +ssl_email: "admin@example.com" +ssl_cert_resolver: "dns" # Certificate resolver name + +# Certificate mode: 'acme' for Let's Encrypt with DNS challenge or 'selfsigned' for self-signed certs +cert_mode: "selfsigned" # Use selfsigned for vagrant, acme for production + +# ACME DNS Challenge with RFC2136 (TSIG) configuration +acme_dns_zone: "" # e.g., "digitalboard._acme.digitalboard.ch." +acme_dns_nameserver: "" # e.g., "192.168.1.1:53" +acme_tsig_algorithm: "hmac-sha256" +acme_tsig_key: "" # TSIG key name +acme_tsig_secret: "" # TSIG secret +acme_propagation_timeout: "120" +acme_polling_interval: "2" +acme_ttl: "60" + +# Self-signed certificate configuration (for vagrant/testing) +selfsigned_cert_dir: "{{ docker_volume_dir }}/certs" +selfsigned_cert_days: 365 +selfsigned_common_name: "*.local.test" + +# Dashboard +enable_dashboard: false + +# Access log configuration +enable_access_logs: true +access_log_format: "common" +log_level: "INFO" + +# Network name +traefik_network: "proxy" + +# Services to expose (defined by application roles via host_vars or group_vars) +# Each backend server should define this variable with their services +# reverseproxy_services: +# - name: httpbin +# domain: httpbin.example.com +# port: 8080 +# protocol: http # http or https +# entrypoints: [websecure] # optional, defaults based on SSL config + +# DMZ mode: Explicit backend server mapping +# Define which backend servers this DMZ proxy should route to +# If empty or undefined, routes to all servers in backend_servers group +backend_servers_to_proxy: [] +# Example: +# backend_servers_to_proxy: +# - backend1 +# - backend2 \ No newline at end of file diff --git a/roles/reverseproxy/meta/main.yml b/roles/reverseproxy/meta/main.yml index 36b9858..5c93db2 100644 --- a/roles/reverseproxy/meta/main.yml +++ b/roles/reverseproxy/meta/main.yml @@ -30,6 +30,5 @@ galaxy_info: # NOTE: A tag is limited to a single word comprised of alphanumeric characters. # Maximum 20 tags per role. -dependencies: [] - # List your role dependencies here, one per line. Be sure to remove the '[]' above, - # if you add dependencies to this list. +dependencies: + - digitalboard.core.base diff --git a/roles/reverseproxy/tasks/main.yml b/roles/reverseproxy/tasks/main.yml index 3481bb2..3721b3d 100644 --- a/roles/reverseproxy/tasks/main.yml +++ b/roles/reverseproxy/tasks/main.yml @@ -2,23 +2,23 @@ --- # tasks file for reverseproxy -- name: Gather service information from all hosts - setup: - delegate_to: "{{ item }}" - delegate_facts: true - loop: "{{ groups['all_servers'] }}" - when: use_static_services | bool - -- name: Build service registry from all hosts +- name: Determine which backend servers to proxy (DMZ mode) set_fact: - all_services: "{{ all_services | default([]) + hostvars[item].services | default([]) | map('combine', {'backend_host': item}) | list }}" - loop: "{{ groups['all_servers'] }}" - when: use_static_services | bool + _backend_servers: "{{ backend_servers_to_proxy if backend_servers_to_proxy | length > 0 else groups['backend_servers'] | default([]) }}" + when: reverseproxy_mode == 'dmz' + +- name: Build service registry from backend servers (DMZ mode) + set_fact: + proxied_services: "{{ proxied_services | default([]) + hostvars[item].reverseproxy_services | default([]) | map('combine', {'backend_host': hostvars[item].ansible_host | default(item)}) | list }}" + loop: "{{ _backend_servers | default([]) }}" + when: reverseproxy_mode == 'dmz' - name: Debug service registry debug: - var: all_services - when: use_static_services | bool + var: proxied_services + when: + - reverseproxy_mode == 'dmz' + - proxied_services is defined - name: Create docker compose directory file: @@ -26,33 +26,45 @@ state: directory mode: '0755' -- name: Create docker volume directories +- name: Create docker volume directory file: - path: "{{ docker_volume_dir }}/traefik/{{ item }}" + path: "{{ docker_volume_dir }}" state: directory mode: '0755' - loop: - - letsencrypt + +- name: Create traefik config directory + file: + path: "{{ docker_volume_dir }}/config" + state: directory + mode: '0755' + when: reverseproxy_mode == 'dmz' + +- name: Create letsencrypt directory + file: + path: "{{ docker_volume_dir }}/letsencrypt" + state: directory + mode: '0755' + when: cert_mode == 'acme' - name: Create traefik Docker network community.docker.docker_network: - name: traefik + name: "{{ traefik_network }}" state: present - name: Generate traefik static configuration template: src: traefik.yml.j2 - dest: "{{ docker_compose_dir }}/traefik.yml" + dest: "{{ docker_volume_dir }}/traefik.yml" mode: '0644' notify: restart traefik -- name: Generate traefik services configuration for discovered services +- name: Generate traefik dynamic configuration for DMZ services template: src: services.yml.j2 - dest: "{{ docker_compose_dir }}/services.yml" + dest: "{{ docker_volume_dir }}/config/services.yml" mode: '0644' notify: restart traefik - when: use_static_services | bool + when: reverseproxy_mode == 'dmz' - name: Create docker-compose file for traefik template: @@ -63,4 +75,4 @@ - name: Start traefik container community.docker.docker_compose_v2: project_src: "{{ docker_compose_dir }}" - state: present + state: present \ No newline at end of file diff --git a/roles/reverseproxy/templates/docker-compose.yml.j2 b/roles/reverseproxy/templates/docker-compose.yml.j2 index 3fe053f..9c1cc93 100644 --- a/roles/reverseproxy/templates/docker-compose.yml.j2 +++ b/roles/reverseproxy/templates/docker-compose.yml.j2 @@ -1,21 +1,39 @@ services: traefik: - image: traefik:v3.5 - container_name: traefik + image: traefik:latest + container_name: reverseproxy restart: always +{% if cert_mode == 'acme' %} + environment: + RFC2136_NAMESERVER: "{{ acme_dns_nameserver }}" + RFC2136_TSIG_ALGORITHM: "{{ acme_tsig_algorithm }}" + RFC2136_TSIG_KEY: "{{ acme_tsig_key }}" + RFC2136_TSIG_SECRET: "{{ acme_tsig_secret }}" + RFC2136_PROPAGATION_TIMEOUT: "{{ acme_propagation_timeout }}" + RFC2136_POLLING_INTERVAL: "{{ acme_polling_interval }}" + RFC2136_TTL: "{{ acme_ttl }}" +{% endif %} ports: - "80:80" - "443:443" {% if enable_dashboard %} - - "8080:8080" # Dashboard + - "8080:8080" {% endif %} volumes: - - {{ docker_volume_dir }}/traefik/etc/traefik:/etc/traefik:ro - - {{ docker_volume_dir }}/traefik/letsencrypt:/letsencrypt + - {{ docker_volume_dir }}/traefik.yml:/traefik.yml:ro +{% if cert_mode == 'acme' %} + - {{ docker_volume_dir }}/letsencrypt:/letsencrypt +{% endif %} +{% if reverseproxy_mode == 'dmz' %} + - {{ docker_volume_dir }}/config:/config:ro +{% endif %} +{% if reverseproxy_mode == 'backend' %} - /var/run/docker.sock:/var/run/docker.sock:ro +{% endif %} networks: - - traefik + - {{ traefik_network }} networks: - traefik: + {{ traefik_network }}: + name: {{ traefik_network }} external: true \ No newline at end of file diff --git a/roles/reverseproxy/templates/middlewares.yml.j2 b/roles/reverseproxy/templates/middlewares.yml.j2 index ba022a4..4273796 100644 --- a/roles/reverseproxy/templates/middlewares.yml.j2 +++ b/roles/reverseproxy/templates/middlewares.yml.j2 @@ -1,47 +1,11 @@ -{% if enable_dashboard %} -api: - dashboard: true - insecure: true -{% endif %} - -{% if enable_access_logs %} -accessLog: - format: {{ access_log_format }} -{% endif %} - -entryPoints: - web: - address: ":80" -{% if use_ssl %} - http: - redirections: - entryPoint: - to: websecure - scheme: https -{% endif %} - websecure: - address: ":443" - -providers: -{% if use_static_services | default(false) %} - file: - filename: /etc/traefik/services.yml - watch: true -{% endif %} -{% if use_docker_provider | default(true) %} - docker: - endpoint: "unix:///var/run/docker.sock" - exposedByDefault: false -{% endif %} - -certificatesResolvers: - letsencrypt: - acme: - email: admin@digitalboard.ch - storage: /letsencrypt/acme.json - httpChallenge: - entryPoint: web - -global: - checkNewVersion: false - sendAnonymousUsage: false \ No newline at end of file +http: + middlewares: + secure-headers: + headers: + frameDeny: true + contentTypeNosniff: true + browserXssFilter: true + forceSTSHeader: true + stsSeconds: 31536000 + stsIncludeSubdomains: true + stsPreload: true \ No newline at end of file diff --git a/roles/reverseproxy/templates/services.yml.j2 b/roles/reverseproxy/templates/services.yml.j2 index 98eecad..7b8c6dd 100644 --- a/roles/reverseproxy/templates/services.yml.j2 +++ b/roles/reverseproxy/templates/services.yml.j2 @@ -1,30 +1,35 @@ -{% if use_static_services | default(false) %} http: routers: -{% for service in all_services %} +{% for service in proxied_services %} {{ service.name }}: rule: "Host(`{{ service.domain }}`)" service: {{ service.name }}-service entryPoints: -{% if use_ssl | default(false) %} - - websecure + - {{ 'websecure' if use_ssl else 'web' }} +{% if use_ssl %} tls: - certResolver: letsencrypt +{% if cert_mode == 'acme' %} + certResolver: {{ ssl_cert_resolver }} {% else %} - - web + {} +{% endif %} {% endif %} {% endfor %} services: -{% for service in all_services %} +{% for service in proxied_services %} {{ service.name }}-service: loadBalancer: + passHostHeader: true servers: - - url: "{{ service.upstream_protocol }}://{{ service.backend_host }}:{{ service.port }}" -{% if service.health_check is defined %} - healthCheck: - path: "{{ service.health_check }}" - interval: "30s" + - url: "{{ service.protocol }}://{{ service.backend_host }}:{{ service.port }}" +{% if service.protocol == 'https' and cert_mode == 'selfsigned' %} + serversTransport: insecure-transport {% endif %} {% endfor %} + +{% if cert_mode == 'selfsigned' %} + serversTransports: + insecure-transport: + insecureSkipVerify: true {% endif %} \ No newline at end of file diff --git a/roles/reverseproxy/templates/traefik.yml.j2 b/roles/reverseproxy/templates/traefik.yml.j2 index 18cc369..cd3d878 100644 --- a/roles/reverseproxy/templates/traefik.yml.j2 +++ b/roles/reverseproxy/templates/traefik.yml.j2 @@ -1,9 +1,17 @@ +log: + level: {{ log_level }} + {% if enable_dashboard %} api: dashboard: true insecure: true {% endif %} +{% if enable_access_logs %} +accessLog: + format: {{ access_log_format }} +{% endif %} + entryPoints: web: address: ":80" @@ -18,24 +26,36 @@ entryPoints: address: ":443" providers: -{% if use_static_services | default(false) %} +{% if reverseproxy_mode == 'dmz' %} file: - filename: /etc/traefik/services.yml + directory: /config watch: true {% endif %} -{% if use_docker_provider | default(true) %} +{% if reverseproxy_mode == 'backend' %} docker: endpoint: "unix:///var/run/docker.sock" + network: {{ traefik_network }} exposedByDefault: false {% endif %} +{% if use_ssl and cert_mode == 'acme' %} certificatesResolvers: - letsencrypt: + {{ ssl_cert_resolver }}: acme: - email: admin@digitalboard.ch + email: {{ ssl_email }} storage: /letsencrypt/acme.json - httpChallenge: - entryPoint: web + dnsChallenge: + provider: rfc2136 + resolvers: + - "{{ acme_dns_nameserver }}" +{% endif %} + +{% if use_ssl %} +tls: + options: + default: + minVersion: VersionTLS12 +{% endif %} global: checkNewVersion: false