job "traefik" { datacenters = ["dc1"] region = "global" group "traefik" { count = 2 shutdown_delay = "6s" constraint { operator = "distinct_hosts" value = "true" } ephemeral_disk { # Use minimal ephemeral disk size = 101 } network { mode = "bridge" port "http" { static = 80 to = 5080 } port "https" { static = 443 to = 5443 } } service { name = "traefik-sidecar" port = "https" connect { sidecar_service { } sidecar_task { config { args = [ "-c", "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json", "-l", "${meta.connect.log_level}", "--concurrency", "${meta.connect.proxy_concurrency}", "--disable-hot-restart" ] } resources { cpu = 50 memory = 64 } } } } service { name = "traefik" port = "https" task = "traefik" meta { alloc = "${NOMAD_ALLOC_INDEX}" datacenter = "${NOMAD_DC}" group = "${NOMAD_GROUP_NAME}" job = "${NOMAD_JOB_NAME}" namespace = "${NOMAD_NAMESPACE}" node = "${node.unique.name}" region = "${NOMAD_REGION}" } # Traefik supports native Consul service mesh connect { native = true } tags = [ "traefik.http.routers.traefik-api.rule=(Host(`traefik.example.org`) || HostRegexp(`(.+\\.)?traefik.service.consul`)) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))", "traefik.http.routers.traefik-api.service=api@internal", "traefik.enable=true", "traefik.http.routers.traefik-api.entrypoints=https", "traefik.http.middlewares.csp-traefik-api.headers.contentsecuritypolicy=default-src 'self';font-src 'self' data:;img-src 'self' data:;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';", "traefik.http.middlewares.traefik-api-99-path.replacepathregex.regex=^/dashboard/(.*)", "traefik.http.middlewares.traefik-api-99-path.replacepathregex.replacement=/dashboard/$${1}", "traefik.http.routers.traefik-api.middlewares=security-headers@file,rate-limit-std@file,forward-proto@file,inflight-std@file,hsts@file,compression@file,traefik-api-99-path,csp-traefik-api", "traefik.http.routers.traefik-ping.rule=(Host(`traefik.example.org`) || HostRegexp(`(.+\\.)?traefik.service.consul`)) && Path(`/ping`) && Method(`GET`)", "traefik.http.routers.traefik-ping.service=ping@internal", "traefik.enable=true", "traefik.http.routers.traefik-ping.entrypoints=http,https", "traefik.http.routers.traefik-ping.priority=2000", "traefik.http.middlewares.csp-traefik-ping.headers.contentsecuritypolicy=default-src 'self';font-src 'self' data:;img-src 'self' data:;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';", "traefik.http.routers.traefik-ping.middlewares=security-headers@file,rate-limit-std@file,forward-proto@file,inflight-std@file,hsts@file,compression@file,csp-traefik-ping", "traefik-${NOMAD_ALLOC_INDEX}" ] } task "traefik" { driver = "docker" user = 5443 vault { policies = ["traefik"] } config { image = "danielberteaud/traefik:3.0.0-rc3-1" command = "traefik" args = [ "--configfile=/secrets/traefik.yml" ] } # Main traefik configuration template { data = <<_EOF log: level: INFO accessLog: bufferingSize: 100 entryPoints: http: address: ":{{ env "NOMAD_PORT_http" }}" http: redirections: entryPoint: priority: 1000 to: :{{ env "NOMAD_HOST_PORT_https" }} transport: lifeCycle: requestAcceptGraceTimeout: 4 https: address: ":{{ env "NOMAD_PORT_https" }}" http: tls: {} transport: lifeCycle: requestAcceptGraceTimeout: 4 api: dashboard: True providers: consulCatalog: prefix: traefik endpoint: address: {{ sockaddr "GetInterfaceIP \"nomad\"" }}:8500 scheme: http token: {{ with secret "consul/creds/traefik" }}{{ .Data.token }}{{ end }} exposedByDefault: False connectAware: True connectByDefault: True serviceName: traefik refreshInterval: 5s watch: True file: directory: /secrets/config watch: True ping: manualRouting: True _EOF destination = "secrets/traefik.yml" perms = "0400" uid = 105443 gid = 100000 } # Dynamic file configuration template { data = <<_EOF --- {{ if gt (len (secrets "kv/service/traefik/basicauth/")) 0 }} http: middlewares: {{- range secrets "kv/service/traefik/basicauth/" }} basicauth-{{ . }}: basicAuth: realm: {{ . }} removeheader: true users: {{- with secret (printf "kv/data/service/traefik/basicauth/%s" .) }} {{- range $k, $v := .Data.data }} - {{ $k }}:{{ if $v | regexMatch "^\\$2y\\$" }}{{ $v }}{{ else }}{{ sprig_bcrypt $v }}{{ end }} {{- end }} {{- end }} {{- end }} {{- end }} _EOF destination = "secrets/config/basicauth.yml" change_mode = "noop" perms = "0400" uid = 105443 gid = 100000 } template { data = <<_EOF --- _EOF destination = "secrets/config/lemonldap.yml" change_mode = "noop" perms = "0400" uid = 105443 gid = 100000 } template { data = <<_EOF --- {{- if ne 0 (len (secrets "kv/service/traefik/certs/")) }} tls: certificates: {{- range secrets "kv/service/traefik/certs/" }} {{- $cn := . }} {{- with secret (printf "kv/service/traefik/certs/%s" $cn) }} # {{ $cn }} - certFile: |- {{ .Data.data.cert | replaceAll "\n\n" "\n" | indent 8 }} keyFile: |- {{ .Data.data.key | indent 8 }} {{- end }} {{- end }} {{- end }} _EOF destination = "secrets/config/certificates.yml" change_mode = "noop" perms = "0400" uid = 105443 gid = 100000 } template { data = <<_EOF --- {{ if ne 0 (len (ls "common/ip")) }} http: middlewares: {{ range ls "common/ip" }} ip-{{ .Key }}: ipAllowList: sourceRange:{{ range .Value | parseYAML }} {{- if . | regexMatch "^include:.*" }} # Include IP from {{ . | replaceAll "include:" "" }} {{- range key (printf "common/ip/%s" . | replaceAll "include:" "") | parseYAML }} - {{ . }}{{ end }} {{- else }} - {{ . }}{{ end }}{{ end }} {{ end }} tcp: middlewares: {{ range ls "common/ip" }} ip-{{ .Key }}: ipAllowList: sourceRange:{{ range .Value | parseYAML }} {{- if . | regexMatch "^include:.*" }} # Include IP from {{ . | replaceAll "include:" "" }} {{- range key (printf "common/ip/%s" . | replaceAll "include:" "") | parseYAML }} - {{ . }}{{ end }} {{- else }} - {{ . }}{{ end }}{{ end }} {{ end }} {{ end }} _EOF destination = "secrets/config/ip.yml" change_mode = "noop" perms = "0400" uid = 105443 gid = 100000 } template { data = <<_EOF --- http: middlewares: autodetect: contentType: {} compression: compress: excludedContentTypes: - image/png - image/jpeg - image/jpg - image/pjpeg - image/avif - image/webp - image/x-icon - font/woff2 - font/woff - video/webm - video/ogg - video/mpeg - video/mp4 - video/3gpp - video/3gpp2 - video/ogg - video/x-flv - video/h261 - video/h263 - video/h264 - video/jpm - video/jpeg - video/quicktime - audio/x-aac - audio/x-aiff - audio/mpeg - audio/mp4 - audio/ogg - audio/webm - audio/opus - application/zip - application/gzip - application/x-7z-compressed - application/x-ace-compressed - application/x-debian-package - application/vnd.android.package-archive _EOF destination = "secrets/config/performance.yml" change_mode = "noop" perms = "0400" uid = 105443 gid = 100000 } template { data = <<_EOF --- http: middlewares: rate-limit-std: rateLimit: average: 30 burst: 50 rate-limit-high: rateLimit: average: 100 burst: 200 inflight-std: inFlightReq: amount: 100 inflight-high: inFlightReq: amount: 300 security-headers: headers: contentTypeNosniff: True browserXssFilter: True # customFrameOptionsValue: sameorigin customResponseHeaders: Server: "" X-Powered-By: "" X-Envoy-Upstream-Service-Time: "" hsts: headers: forceSTSHeader: True stsIncludeSubdomains: True stsSeconds: 63072000 stsPreload: True csp-strict: headers: contentSecurityPolicy: "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'" csp-relaxed: headers: contentSecurityPolicy: "default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:" _EOF destination = "secrets/config/security.yml" change_mode = "noop" perms = "0400" uid = 105443 gid = 100000 } template { data = <<_EOF --- http: middlewares: forward-proto: headers: customRequestHeaders: X-Forwarded-Proto: https _EOF destination = "secrets/config/proxy.yml" change_mode = "noop" perms = "0400" uid = 105443 gid = 100000 } resources { cpu = 500 memory = 256 memory_max = 300 } } } } # vim: syntax=hcl