From 314fce4757ff0fcbd31c89f29167e9a61362c884 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 | 47 ++++++++++++++ roles/reverseproxy/templates/services.yml.j2 | 29 +++++---- roles/reverseproxy/templates/traefik.yml.j2 | 34 ++++++++-- 7 files changed, 213 insertions(+), 57 deletions(-) create mode 100644 roles/reverseproxy/templates/middlewares.yml.j2 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 new file mode 100644 index 0000000..ba022a4 --- /dev/null +++ b/roles/reverseproxy/templates/middlewares.yml.j2 @@ -0,0 +1,47 @@ +{% 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 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