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/06/03 15:17] walbaumserver:traefik [2025/09/25 15:05] (aktuell) walbaum
Zeile 1: Zeile 1:
 ====== Traefik ====== ====== Traefik ======
  
-  ; URL : [[https://traefik.smns-bw.org/|https://traefik.smns-bw.org/]]  +  ; URL : [[https://traefik.smns-bw.org/|https://traefik.smns-bw.org/]] 
-  ; User : smns-tr+  ; User : smns-tr 
   ; Passwort : <decrypt>U2FsdGVkX18v7/dGh8b4MGuIQ8RPzXqjcWSlZ/AaEFgCQeNQcE1AYQxEWGLdiM9Y</decrypt>   ; Passwort : <decrypt>U2FsdGVkX18v7/dGh8b4MGuIQ8RPzXqjcWSlZ/AaEFgCQeNQcE1AYQxEWGLdiM9Y</decrypt>
   ; Production : hetzner:/opt/traefik/   ; Production : hetzner:/opt/traefik/
  
- +<code> 
-├── 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 
-│ ├── {{:server:docker-compose.yml|docker-compose.yml}} \\ +│ ├── docker-compose.yml 
-│ ├── .env\\ +│ ├── .env 
-│ ├── traefik.log\\ +│ ├── traefik.log 
-│ ├── {{:server:traefik.yml|traefik.yml}} \\+│ ├── traefik.yml
 │ ├── access.log │ ├── access.log
 +</code>
  
 In .env steht die URL traefik.smns-bw.org sowie die Zugangsdaten für diese Seite für docker-compose.yml In .env steht die URL traefik.smns-bw.org sowie die Zugangsdaten für diese Seite für docker-compose.yml
  
 In die einzelnen docker-compose.yml Files der Container kommt dann sowas (Beispiel Sammlungskatalog): In die einzelnen docker-compose.yml Files der Container kommt dann sowas (Beispiel Sammlungskatalog):
- 
 <code> <code>
 +
     labels:     labels:
       - "traefik.http.routers.webportal.rule=Host(`${URL}`)"       - "traefik.http.routers.webportal.rule=Host(`${URL}`)"
Zeile 36: Zeile 37:
       - "traefik.http.services.webportal.loadbalancer.sticky.cookie.secure=true"       - "traefik.http.services.webportal.loadbalancer.sticky.cookie.secure=true"
       - "traefik.docker.network=proxy"       - "traefik.docker.network=proxy"
 +
 </code> </code>
  
-Damit der Docker.sock nicht nach außen exposed ist, ist zusammen mit Traefik ein docker-socket aufgesetzt, der von hier stammt: https://github.com/wollomatic/socket-proxy+Damit der Docker.sock nicht nach außen exposed ist, ist zusammen mit Traefik ein docker-socket aufgesetzt, der von hier stammt: [[https://github.com/wollomatic/socket-proxy|https://github.com/wollomatic/socket-proxy]]
  
 +<codedoc toggle docker-compose.yml>
 +services:
 +  traefik:
 +    container_name: traefik
 +    image: "traefik:latest"
 +    restart: always
 +#    read_only: true
 +    command:
 +      - --configfile=/traefik.yml
 +    mem_limit: 2G
 +    cpus: 0.75
 +    ports:
 +      - "80:80"
 +      - "443:443"
 +    volumes:
 +      - "./acme.json:/acme.json"
 +      - "./traefik.yml:/traefik.yml:ro"
 +      - "./configfiles:/configfiles:ro"
 +#      - ".logs/traefik.log:/traefik.log"
 +#      - ".logs/access.log:/access.log"
 +      - "./logs:/logs:rw"
 +    depends_on:
 +      - "dockerproxy"
 +    security_opt:
 +      - "no-new-privileges:true"
 +    networks:
 +      - "proxy"
 +      - "docker-proxynet"
 +    labels:
 +      - "traefik.http.routers.traefik.entrypoints=https"
 +      - "traefik.http.routers.traefik.rule=Host(`traefik.smns-bw.org`)"
 +      - "traefik.http.routers.traefik.tls=true"
 +      - "traefik.http.routers.traefik.tls.certresolver=leresolver"
 +      # API service
 +      - "traefik.http.routers.traefik.service=api@internal"
 +      - "traefik.http.routers.traefik.middlewares=secure-traefik@file"
 +      - "traefik.http.services.traefik.loadbalancer.sticky=true"
 +      - "traefik.http.services.traefik.loadbalancer.sticky.cookie.httpOnly=true"
 +      - "traefik.http.services.traefik.loadbalancer.sticky.cookie.secure=true"
 +    healthcheck:
 +      test: ["CMD", "wget", "--spider", "http://localhost:8082/ping"]
 +      interval: 30s
 +      timeout: 5s
 +      retries: 3
 +      start_period: 10s
 +
 +  dockerproxy:
 +    build:
 +      context: .
 +    container_name: socket-proxy
 +    command:
 +      - '-loglevel=DEBUG'
 +      - '-allowfrom=traefik,172.31.0.1'
 +      - '-listenip=0.0.0.0'
 +      - '-allowGET=/v1\..{1,2}/(version|containers/.*|events.*)'
 +      - '-shutdowngracetime=5'
 +    restart: unless-stopped
 +    user: "65534:998"
 +    read_only: true
 +    mem_limit: 64M
 +    cap_drop:
 +      - ALL
 +    security_opt:
 +      - no-new-privileges
 +    volumes:
 +      - /var/run/docker.sock:/var/run/docker.sock:ro
 +    networks:
 +      - "proxy"
 +      - "docker-proxynet"
 +    healthcheck:
 +      test: [ "CMD", "nc", "-z", "localhost", "2375" ]
 +      interval: 1m
 +      timeout: 3s
 +      retries: 3
 +
 +  error-pages-webportal:
 +    container_name: error-webportal
 +    image: nginx:alpine
 +    restart: always
 +    volumes:
 +      - ./nginx.conf:/etc/nginx/nginx.conf:ro
 +      - ./mime.types:/etc/nginx/mime.types
 +      - ./error-pages-webportal:/usr/share/nginx/html:ro
 +    labels:
 +      - "traefik.http.routers.error-pages-webportal.entrypoints=https"
 +      - "traefik.http.routers.error-pages-webportal.tls=true"
 +      - "traefik.http.routers.error-pages-webportal.tls.certresolver=leresolver"
 +      - "traefik.http.routers.error-pages-webportal.rule=Host(`error-webportal.smns-bw.org`)"
 +#      - "traefik.http.routers.error-pages-webportal.middlewares=secure-global@file"
 +      - "traefik.http.services.error-pages-webportal.loadbalancer.sticky=true"
 +      - "traefik.http.services.error-pages-webportal.loadbalancer.sticky.cookie.httpOnly=true"
 +      - "traefik.http.services.error-pages-webportal.loadbalancer.sticky.cookie.secure=true"
 +      - "traefik.http.services.error-pages-webportal.loadbalancer.server.port=80"
 +    networks:
 +      - "proxy"
 +    healthcheck:
 +      test: ["CMD", "curl", "-f", "http://localhost/healthz.html"]
 +      interval: 30s
 +      timeout: 5s
 +      retries: 3
 +      start_period: 10s
 +
 +networks:
 +  proxy:
 +    external: true
 +  docker-proxynet:
 +    driver: bridge
 +    internal: true
 +</codedoc>
 +\\
 +<codedoc toggle traefik.yml>
 +entryPoints:
 +  http:
 +    address: ":80"
 +    http:
 +      redirections:
 +        entryPoint:
 +          to: ':443'
 +          scheme: https
 +  https:
 +    address: ":443"
 +#    http3:
 +#      advertisedPort: 443
 +  postgres:
 +    address: ":5434"
 +  ssh:
 +    address: ":666"
 +  healthcheck:
 +    address: ":8082"
 +log:
 +  level: INFO
 +  filePath: "/logs/traefik.log"
 +  format: json
 +
 +accessLog:
 +  filePath: "/logs/access.log"
 +  bufferingSize: 100
 +
 +api: {}
 +
 +ping:
 +  entryPoint: healthcheck
 +
 +providers:
 +  docker:
 +#    endpoint: "tcp://socket-proxy:2375"
 +    endpoint: "tcp://dockerproxy:2375"
 +#    endpoint: "unix:///var/run/docker.sock"
 +    watch: true
 +    network: proxy
 +#    exposedByDefault: false
 +  file:
 +    directory: "/configfiles"
 +    watch: true
 +
 +certificatesResolvers:
 +  leresolver:
 +    acme:
 +      email: "it@smns-bw.org"
 +      storage: "/acme.json"
 +      caServer: "https://acme-v02.api.letsencrypt.org/directory"
 +      tlsChallenge: {}
 +
 +experimental:
 +  plugins:
 +    traefik-plugin-cookie-path-prefix:
 +      moduleName: "github.com/SchmitzDan/traefik-plugin-cookie-path-prefix"
 +      version: "v0.0.3"
 +</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 58: Zeile 831:
 === Step-by-Step Setup === === Step-by-Step Setup ===
  
-== 1. Add a dedicated HTTP (non-TLS) entrypoint for health ===+== 1. Add a dedicated HTTP (non-TLS) entrypoint for health ==
  
 Add this to your ''traefik.yml'': Add this to your ''traefik.yml'':
  
-<code yaml>  +<code yaml> 
-entryPoints:  +entryPoints: 
-  healthcheck: +  healthcheck:
     address: ":8082"     address: ":8082"
-    +
 ping: ping:
   entryPoint: healthcheck   entryPoint: healthcheck
 +
 </code> </code>
  
Zeile 74: Zeile 848:
   * Do not enable TLS or configure HTTP redirection for this entrypoint.   * Do not enable TLS or configure HTTP redirection for this entrypoint.
  
-== 2. Update ''docker-compose.yml'' healthcheck section ===+== 2. Update ''docker-compose.yml'' healthcheck section == 
 + 
 +<code yaml> 
 +healthcheck: 
 +  test: [ "CMD", "wget", "--spider", "http://localhost:8082/ping"
 +  interval: 30s 
 +  timeout: 5s 
 +  retries: 3 
 +  start_period: 10s
  
-<code yaml>  
-healthcheck:  
-  test: [ "CMD", "wget", "--spider", "http://localhost:8082/ping" 
-  interval: 30s  
-  timeout: 5s  
-  retries: 3  
-  start_period: 10s  
 </code> </code>
  
-== 3. Recreate the container (!important) ===+== 3. Recreate the container (!important) ==
  
 After editing the healthcheck, you must remove and recreate the container to apply the updated check. After editing the healthcheck, you must remove and recreate the container to apply the updated check.
  
-<code bash> docker compose down docker compose up -d </code> or for just the traefik service: <code bash> docker compose rm traefik docker compose up -d traefik </code>+<code bash> 
 + docker compose down docker compose up -d 
 + 
 +</code> 
 + 
 +or for just the traefik service: 
 + 
 +<code bash> 
 + docker compose rm traefik docker compose up -d traefik 
 + 
 +</code>
  
-== 4. Confirm it's working ===+== 4. Confirm it's working ==
  
   * Check status with:   * Check status with:
-    <code bash> docker inspect traefik | grep Health -A 10 </code>+ 
 +<code bash> 
 + docker inspect traefik | grep Health -A 10 
 + 
 +</code> 
   * Look for:   * Look for:
-    * ''"Status": "healthy"'' +      * ''"Status": "healthy"'' 
-    * The test pointing at ''http://localhost:8082/ping''+      * The test pointing at ''[[http://localhost:8082/ping|http://localhost:8082/ping]]''
   * You can also exec into the container:   * You can also exec into the container:
-    <code bash> wget --spider http://localhost:8082/ping </code>+ 
 +<code bash> 
 + wget --spider http://localhost:8082/ping 
 + 
 +</code> 
   * and expect “remote file exists” or HTTP 200.   * and expect “remote file exists” or HTTP 200.
  
Zeile 105: Zeile 900:
  
   * If you see '''404 Not Found''' or status stays ''unhealthy'', check:   * If you see '''404 Not Found''' or status stays ''unhealthy'', check:
-    * The entryPoint in ping and traefik.yml matches (healthcheck) +      * The entryPoint in ping and traefik.yml matches (healthcheck) 
-    * Logs for ping endpoint registration (grep -i ping <traefik.log>+      * Logs for ping endpoint registration (grep -i ping <traefik.log>
-    * Healthcheck in the running container is updated (see ''docker inspect'')+      * Healthcheck in the running container is updated (see ''docker inspect'')
   * If the healthcheck is still using the old endpoint (e.g., port 443), the container must be removed and recreated.   * If the healthcheck is still using the old endpoint (e.g., port 443), the container must be removed and recreated.
  
Zeile 113: Zeile 908:
  
   * Q: Why not use /ping on :443?   * Q: Why not use /ping on :443?
-    * A: Traefik 3.x forbids it; /ping only works on a non-TLS (HTTP) entrypoint.+      * A: Traefik 3.x forbids it; /ping only works on a non-TLS (HTTP) entrypoint.
   * Q: Do I need to expose port 8082 externally?   * Q: Do I need to expose port 8082 externally?
-    * A: No; healthchecks run inside the container.+      * A: No; healthchecks run inside the container.
   * Q: Can I combine ping and redirect on the same entrypoint?   * Q: Can I combine ping and redirect on the same entrypoint?
-    * A: No; keep your healthcheck entrypoint plain.+      * A: No; keep your healthcheck entrypoint plain.
  
 === References === === References ===
Zeile 125: 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 — {{date}}+Authored for SMNS IT by Chattie and AI Programmer 
 +