Metainformationen zur Seite
  •  

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen RevisionVorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
server:traefik [2025/09/12 10:40] walbaumserver:traefik [2025/09/25 15:05] (aktuell) walbaum
Zeile 9: Zeile 9:
 ├── traefik ├── traefik
 │ ├── configfiles │ ├── configfiles
-│ │ ├── {{:server:config.yml|config.yml}}  +│ │ ├── config.yml 
-│ │ ├── {{:server:middleware-chains.yml|middleware-chains.yml}}  +│ │ ├── middleware-chains.yml 
-│ │ ├── {{:server:middlewares.yml|middlewares.yml}}  +│ │ ├── middlewares.yml 
-│ │ ├── {{:server:tls-opts.yml|tls-opts.yml}}+│ │ ├── tls-opts.yml
 │ ├── docker-compose.yml │ ├── docker-compose.yml
 │ ├── .env │ ├── .env
Zeile 150: Zeile 150:
     driver: bridge     driver: bridge
     internal: true     internal: true
-</codedoc>\\+</codedoc>
 \\ \\
 <codedoc toggle traefik.yml> <codedoc toggle traefik.yml>
Zeile 211: Zeile 211:
       version: "v0.0.3"       version: "v0.0.3"
 </codedoc> </codedoc>
 +\\
 +<codedoc toggle config.yml>
 +http:
 +  routers:
 +    webmin:
 +      entryPoints:
 +        - "https"
 +      rule: Host(`webmin.smns-bw.org`)
 +      service: webmin
 +      middlewares:
 +        - "secure-webmin"
 +      tls:
 +        certResolver: leresolver
 +        options: tls-opts
 +
 +    traefik:
 +      rule: Host(`traefik.smns-bw.org`)
 +      service: cookies
 +
 +    smns_stats:
 +      rule: Host(`statistics.smns-bw.org`)
 +#      rule: "PathPrefix(`/statistics`)"
 +      service: smns_stats
 +      entryPoints:
 +        - "https"
 +      middlewares:
 +        - "secure-collections"
 +      tls:
 +        certResolver: leresolver
 +        options: tls-opts
 +
 +  services:
 +    webmin:
 +      loadBalancer:
 +        servers:
 +          - url: "http://172.18.0.1:10000/"
 +        passHostHeader: true
 +        sticky:
 +          cookie:
 +            secure: true
 +            httpOnly: true
 +            sameSite: lax
 +
 +    smns_stats:
 +      loadBalancer:
 +        servers:
 +          - url: "http://172.18.0.1:8050"
 +        passHostHeader: true
 +
 +    cookies:
 +      loadBalancer:
 +        sticky:
 +          cookie:
 +            secure: true
 +            httpOnly: true
 +            sameSite: lax
 +</codedoc>
 +\\
 +<codedoc toggle middleware-chains.yml>
 +http:
 +  middlewares:
 +    secure-global:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-base"
 +
 +    secure-traefik:
 +      chain:
 +        middlewares:
 +          - "traefik-auth"               # auth first, so errors still get headers/CSP
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-base"
 +
 +    secure-idservice:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-idservice"
 +
 +    secure-biocase:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-biocase"
 +
 +    secure-webmin:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-webmin"
 +
 +    secure-prestashop:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-prestashop"
 +
 +    secure-ent:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-ent"
 +
 +    secure-collections:
 +      chain:
 +        middlewares:
 +          - "webportal-errors"            # moved before headers so error pages get hardened
 +          - "security-headers-hsts-subdomains"
 +          - "security-headers-allowframes"
 +          - "cors-collections"
 +          - "CSP-collections"
 +
 +    secure-digiphyll:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "security-headers-allowframes"
 +          - "cors-default"
 +          - "CSP-digiphyll"
 +
 +    secure-dokuwiki:
 +      chain:
 +        middlewares:
 +          - "security-headers-hsts-subdomains"
 +          - "security-headers-allowframes"
 +          - "cors-default"
 +          - "CSP-dokuwiki"
 +
 +    secure-geometroidea:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-geometroidea"
 +
 +    secure-h5p:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "security-headers-allowframes"
 +          - "cors-default"
 +          - "CSP-h5p"
 +
 +    secure-awstats:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "cors-default"
 +          - "CSP-awstats"
 +
 +    secure-librechat:
 +      chain:
 +        middlewares:
 +          - "security-headers-noindex"
 +          - "cors-default"
 +          - "CSP-librechat"
 +
 +    secure-pictures:
 +      chain:
 +        middlewares:
 +          - "security-headers-base"
 +          - "security-headers-allowframes"
 +          - "cors-default"
 +          - "CSP-base"
 +</codedoc>
 +\\
 +<codedoc toggle middlewares.yml>
 +http:
 +  middlewares:
 +    default-whitelist:
 +      ipWhiteList:
 +        sourceRange:
 +          - "127.0.0.1/32"
 +          - "172.19.0.0/24"
 +
 +    traefik-auth:
 +      basicAuth:
 +        users:
 +          - "smns-tr:$2y$10$mUF04Rf7852wnP5xGxewte1hW4X/LcEN2c7of5xPOEyzbAXGVrEAi"
 +
 +    # Core security baseline used by most services
 +    security-headers-base:
 +      headers: &SEC_BASE
 +      addVaryHeader: true
 +
 +      # Response header hardening
 +      contentTypeNosniff: true
 +      browserXssFilter: true
 +      referrerPolicy: "same-origin"
 +      permissionsPolicy: fullscreen=(self "https://smns-bw.org"), geolocation=*, midi=(), camera=(), usb=(), magnetometer=(), accelerometer=(), vr=(), speaker=(), ambient-light-sensor=(), gyroscope=(), microphone=(), payment=()
 +
 +      # HSTS
 +      forceSTSHeader: true
 +      stsSeconds: 31536000
 +      stsPreload: false
 +      # Keep includeSubdomains off in base to avoid accidental lock-in; use variant below when desired.
 +      # stsIncludeSubdomains: false
 +
 +      # X-Frame-Options — deny by default (use CSP frame-ancestors for granular control)
 +      frameDeny: true
 +      # customFrameOptionsValue: "SAMEORIGIN"  # optional alternative if you want XFO too
 +
 +      # TLS/Proxy awareness
 +      sslRedirect: true                # FIX: belongs at headers root (not inside sslProxyHeaders)
 +      sslProxyHeaders:
 +        X-Forwarded-Proto: "https"
 +      hostsProxyHeaders:
 +        - "X-Forwarded-Host"
 +
 +      # Standard response scrubbing
 +      customResponseHeaders: &RESP_BASE
 +        Server: ""                     # FIX: unify case
 +        X-Powered-By: ""               # FIX: unify case
 +        X-Robots-Tag: "index, follow"  # override via -noindex variant
 +
 +      # Upstream request adjustments (to your apps)
 +      customRequestHeaders: &REQ_BASE
 +        X-Forwarded-Proto: "https"     # OK to keep here
 +
 +    # Variant: noindex
 +    security-headers-noindex:
 +      headers:
 +      <<: *SEC_BASE
 +      customResponseHeaders:
 +        <<: *RESP_BASE
 +        X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
 +
 +    # Variant: allow framing (prefer CSP frame-ancestors to control who can frame you)
 +    security-headers-allowframes:
 +      headers:
 +      <<: *SEC_BASE
 +      frameDeny: false
 +      # customFrameOptionsValue is mostly ignored by modern browsers except SAMEORIGIN/DENY.
 +      # "ALLOW-FROM" is deprecated; rely on CSP if you need granular control.
 +      customFrameOptionsValue: "SAMEORIGIN"
 +
 +    # Variant: HSTS include subdomains, longer max-age
 +    security-headers-hsts-subdomains:
 +      headers:
 +      <<: *SEC_BASE
 +      stsIncludeSubdomains: true
 +      stsSeconds: 63072000
 +
 +    # Optional: shared Cache-Control for static-ish services
 +    cache-public:
 +      headers:
 +      customResponseHeaders:
 +        Cache-Control: "public"         # FIX: was (incorrectly) in customRequestHeaders
 +
 +    # CORS: compose these with the security headers as needed
 +    cors-default:
 +      headers: &CORS_DEFAULT
 +      accessControlAllowMethods:
 +        - GET
 +        - OPTIONS
 +        - PUT
 +      accessControlAllowHeaders: "*"
 +      accessControlMaxAge: 100
 +      addVaryHeader: true
 +
 +    cors-collections:
 +      headers:
 +      <<: *CORS_DEFAULT
 +      accessControlAllowOriginList:
 +        - https://pydeepzoom.smns-bw.org
 +        - https://pictures.smns-bw.org
 +
 +    # Base CSP used by services that only need same-origin + *.smns-bw.org
 +    CSP-base:
 +      headers:
 +      contentSecurityPolicy: &CSP_BASE >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' data:;
 +        connect-src 'self';
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline';
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +
 +    # If you still reference CSP-global anywhere, just point it to the base:
 +    CSP-global:
 +      headers:
 +      contentSecurityPolicy: *CSP_BASE
 +
 +    # Deltas below (only where different from base)
 +
 +    CSP-dokuwiki:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org *.google.com;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' https://www.dokuwiki.org https://www.gravatar.com data:;
 +        connect-src 'self';
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline';
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +
 +    CSP-idservice:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' https://physalia.evolution.uni-bonn.de data:;
 +        connect-src 'self';
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline';
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org https://physalia.evolution.uni-bonn.de;
 +
 +    CSP-librechat:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' blob: data:;
 +        connect-src 'self';
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline';
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +
 +    # Fixed: removed invalid 'unsafe-inline' and 'unsafe-eval' from -elem directives
 +    CSP-awstats:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' https://chart.googleapis.com;
 +        connect-src 'self' https://www.gstatic.com/charts/;
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline';
 +        style-src-elem https://www.gstatic.com/charts/;
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        script-src-elem https://www.google.com/jsapi https://www.gstatic.com/charts/;
 +
 +    CSP-h5p:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors https://www.naturkundemuseum-bw.de;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' data:;
 +        connect-src 'self';
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline';
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +
 +    CSP-biocase:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' https://unpkg.com/ https://*.tile.osm.org data:;
 +        connect-src 'self';
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline' https://unpkg.com/leaflet@1.3.3/dist/leaflet.css;
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org https://unpkg.com/leaflet@1.3.3/dist/leaflet.js https://code.jquery.com/jquery-1.7.min.js;
 +
 +    CSP-prestashop:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' data:;
 +        connect-src 'self';
 +        font-src 'self';
 +        style-src 'self' 'unsafe-inline';
 +        style-src-elem https://market.smns-bw.org;
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' https://market.smns-bw.org;
 +
 +    # Base + broader font-src
 +    CSP-webmin:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        base-uri 'self';
 +        img-src 'self' data:;
 +        font-src 'self' *.smns-bw.org data:;
 +        style-src 'self' 'unsafe-inline';
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +
 +    # Fixed: stray "data" token removed, keep single "data:" at end
 +    CSP-collections:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'none';
 +        frame-ancestors 'self' *.smns-bw.org pictures.smns-bw.org;
 +        frame-src *.smns-bw.org;
 +        base-uri 'self';
 +        form-action 'self';
 +        style-src 'self' 'unsafe-inline' *.smns-bw.org https://cloud.ccm19.de;
 +        connect-src 'self' https://cloud.ccm19.de https://matomo.naturkundemuseum-bw.de/;
 +        font-src 'self' *.smns-bw.org;
 +        img-src 'self' *.smns-bw.org https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://cloud.ccm19.de data:;
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' https://pydeepzoom.smns-bw.org https://matomo.naturkundemuseum-bw.de https://cloud.ccm19.de;
 +
 +    # Fixed: removed invalid 'unsafe-inline' from connect-src and from script/style -elem
 +    CSP-ent:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'none';
 +        base-uri 'self';
 +        form-action 'self';
 +        style-src-elem https://ent.smns-bw.org/static/css/;
 +        connect-src 'self' https://matomo.naturkundemuseum-bw.de;
 +        script-src-elem https://matomo.naturkundemuseum-bw.de https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js https://openseadragon.github.io/openseadragon/openseadragon.js;
 +        img-src 'self' https://openseadragon.github.io/openseadragon/images/;
 +
 +    CSP-digiphyll:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org;
 +        frame-ancestors 'self' *.smns-bw.org;
 +        frame-src *.smns-bw.org https://144.41.33.40/;
 +        base-uri 'self';
 +        form-action 'self';
 +        img-src 'self' https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://unpkg.com/ data:;
 +        connect-src 'self';
 +        font-src 'self' data:;
 +        style-src 'self' 'unsafe-inline' data:;
 +        script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org https://mathjax.rstudio.com/latest/ http://144.41.33.40/ data:;
 +
 +    # Fixed: removed invalid 'unsafe-eval' from style-src
 +    CSP-geometroidea:
 +      headers:
 +      contentSecurityPolicy: >
 +        default-src 'self';
 +        style-src 'self' 'unsafe-inline' *.smns-bw.org https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css https://fonts.googleapis.com/css2;
 +        font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/webfonts/ https://fonts.gstatic.com/s/nunito/v25/;
 +        img-src 'self' data:;
 +
 +    my-traefik-plugin-cookie-path-prefix:
 +      plugin:
 +        traefik-plugin-cookie-path-prefix:
 +          prefix: smns
 +
 +    webportal-errors:
 +      errors:
 +        status:
 +          service: error-pages-webportal@docker
 +            - "404-503"
 +          query: "/{status}.html"
 +
 +#    another-service-errors:
 +#      errors:
 +#        status:
 +#          - "404-503"
 +#        service: error-pages-another-service
 +#        query: "/error-pages-another-service/{status}.html"
 +</codedoc>
 +\\
 +<codedoc toggle tls-opts.yml>
 +tls:
 +  options:
 +    tls-opts:
 +      minVersion: VersionTLS12
 +      cipherSuites:
 +        - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
 +        - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
 +        - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
 +        - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
 +        - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
 +        - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
 +      curvePreferences:
 +        - CurveP521
 +        - CurveP384
 +#      sniStrict: true
 +</codedoc>
 +
 +===== Traefik security headers and CSP (DRY config) =====
 +
 +Last updated: 25.09.2025
 +
 +==== High‑level design ====
 +
 +  * One baseline for security headers: security-headers-base
 +  * Small header variants you can compose: -noindex, -allowframes, -hsts-subdomains, cache-public
 +  * Separate CORS middlewares: cors-default, cors-collections
 +  * One base CSP: CSP-base (+ per‑service CSP middlewares that only add what’s different)
 +  * Chains compose the above into per‑service “one-liners” you reference from docker-compose labels.
 +
 +==== Security headers ====
 +
 +=== Baseline (security-headers-base) ===
 +
 +Sets the shared hardening and sane defaults. Highlights:
 +
 +  * addVaryHeader: true
 +  * contentTypeNosniff: true
 +  * browserXssFilter: true
 +  * referrerPolicy: same-origin
 +  * permissionsPolicy: fullscreen=(self "https://smns-bw.org"), geolocation=*, midi=(), camera=(), usb=(), magnetometer=(), accelerometer=(), vr=(), speaker=(), ambient-light-sensor=(), gyroscope=(), microphone=(), payment=()
 +  * HSTS: forceSTSHeader: true, stsSeconds: 31536000, stsPreload: false
 +  * X-Frame-Options: frameDeny: true by default (prefer CSP frame-ancestors for allowlists)
 +  * TLS/proxy awareness: sslRedirect: true; sslProxyHeaders.X-Forwarded-Proto: https
 +  * Scrub response headers (customResponseHeaders): Server: "", X-Powered-By: "", X-Robots-Tag: "index, follow"
 +  * Upstream request header: customRequestHeaders.X-Forwarded-Proto: https
 +Rationale:
 +
 +  * Default deny framing (clickjacking defense); enable per service via -allowframes and control who via CSP.
 +  * Keep includeSubdomains off by default to avoid accidental HSTS lock-in; use the variant when needed.
 +
 +==== Variants you can compose ====
 +
 +  * security-headers-noindex
 +    * Same as base, but X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
 +  * security-headers-allowframes
 +    * Same as base, but frameDeny: false (use CSP frame-ancestors to specify who can frame)
 +  * security-headers-hsts-subdomains
 +    * Same as base, but stsIncludeSubdomains: true, stsSeconds: 63072000
 +  * cache-public
 +    * Adds Cache-Control: public to customResponseHeaders
 +
 +==== CORS middlewares ====
 +
 +  * cors-default
 +    * accessControlAllowMethods: GET, OPTIONS, PUT
 +    * accessControlAllowHeaders: "*"
 +    * accessControlMaxAge: 100
 +    * addVaryHeader: true
 +  * cors-collections
 +    * Inherits cors-default + accessControlAllowOriginList:
 +      * https://pydeepzoom.smns-bw.org
 +      * https://pictures.smns-bw.org
 +
 +==== Content Security Policy (CSP) ====
 +
 +=== Base CSP (CSP-base) ===
 +
 +Applies to most services unchanged.
 +
 +<code yaml> CSP-base: headers: contentSecurityPolicy: &CSP_BASE > default-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org; frame-ancestors 'self' *.smns-bw.org; frame-src *.smns-bw.org; base-uri 'self'; form-action 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.smns-bw.org; </code>
 +
 +Notes:
 +
 +  * Only one CSP header is honored. Don’t stack multiple CSP middlewares in a single chain; “last header wins.”
 +  * Use Report-Only during testing by duplicating to contentSecurityPolicyReportOnly.
 +
 +==== How to add a new service ====
 +
 +  * If defaults are enough:
 +    * Use secure-global chain.
 +  * If it needs framing:
 +    * Use security-headers-allowframes in the chain and set CSP frame-ancestors to the exact allowlist.
 +  * If it needs special CORS:
 +    * Add or create a cors-<service> middleware and include it in the chain.
 +  * If it needs special CSP:
 +    * Copy CSP-base to CSP-<service> and append only the minimal deltas (new hosts/tokens).
 +  * Wire it in docker-compose:
 +    * traefik.http.routers.<name>.middlewares=secure-<name>@file
 +==== Verification (quick commands) ====
 +
 +  * Check headers:
 +    * curl -sI https://<host> | egrep -i "strict-transport-security|x-frame-options|x-content-type-options|referrer-policy|permissions-policy|x-robots-tag"
 +    * Check CSP is present (and only once):
 +    * curl -sI https://<host> | grep -i "content-security-policy"
 +  * Check CORS (preflight example):
 +    * curl -sI -X OPTIONS https://<host>/ -H "Origin: https://example.com" -H "Access-Control-Request-Method: GET"
 +  * Check robots indexing:
 +    * curl -sI https://<host>/ | grep -i x-robots-tag
 +
 +==== Operational notes ====
 +
 +  * Prefer CSP frame-ancestors over X-Frame-Options for precise embedding control.
 +  * Consider a Report-Only CSP during rollouts (duplicate middleware with contentSecurityPolicyReportOnly).
 +  * HSTS includeSubdomains is opt-in via variant to avoid unintentional hard lock-in.
 +  * If a page doesn’t render: check the browser console for CSP violations first; add only the specific host/type needed.
 +==== Appendix: Anchor usage ====
 +We used YAML anchors to stay DRY:
 +
 +  * &SEC_BASE / *SEC_BASE for baseline headers.
 +  * &CSP_BASE / *CSP_BASE for base CSP string.
 +This keeps the source config compact while allowing targeted overrides.
  
 ===== Traefik v3 Healthcheck (Docker) ===== ===== Traefik v3 Healthcheck (Docker) =====
Zeile 318: Zeile 920:
   * [[https://docs.docker.com/reference/compose-file/services/#healthcheck|Docker Compose healthcheck docs]]   * [[https://docs.docker.com/reference/compose-file/services/#healthcheck|Docker Compose healthcheck docs]]
  
-Authored for SMNS IT by Chattie and AI Programmer — [[:server:date?media=server:date|]]+Authored for SMNS IT by Chattie and AI Programmer