Persistent 'Strict-Transport-Security' warning when running NC locally with Docker, Traefik, self-signed cert

Support intro

Sorry to hear you’re facing problems. :slightly_frowning_face:

The community help forum (help(dot)nextcloud(dot)com) is for home and non-enterprise users. Support is provided by other community members on a best effort / “as available” basis. All of those responding are volunteering their time to help you.

If you’re using Nextcloud in a business/critical setting, paid and SLA-based support services can be accessed via portal(dot)nextcloud(dot)com where Nextcloud engineers can help ensure your business keeps running smoothly.

Getting help

In order to help you as efficiently (and quickly!) as possible, please fill in as much of the below requested information as you can.

Before clicking submit: Please check if your query is already addressed via the following resources:

(Utilizing these existing resources is typically faster. It also helps reduce the load on our generous volunteers while elevating the signal to noise ratio of the forums otherwise arising from the same queries being posted repeatedly).

Hey folks! I’m working on setting up a new NC instance in Docker, reverse proxied with Traefik - currently using a self-signed cert to simulate as closely as possible a correct HTTPS configuration before I start opening it up to the wider internet. Currently encountering a ‘Strict-Transport-Authority’ message that I’d like to try to clear.

The Basics

  • Nextcloud version 32.0.5 (from Docker image)
  • OS is Linux Mint 21.3
  • Web server is Apache/2.4.66 (Debian) (from Docker image)
  • Reverse proxy is Traefik 3.6.7
  • PHP version 8.3.30 (from Docker image)
  • Is this the first time you’ve seen this error? Yes
  • When did this problem seem to first start? At install time
  • Installed with Docker Compose (NOT the AIO version - using the apache image)
  • Are you using CloudfIare, mod_security, or similar? No
  • Some additional points potentially worth noting:
    • using Authelia as an auth layer - this is not set up to authenticate NC itself via OAuth yet, currently just gating any access to the page.
    • using a self-signed cert with a self-signed CA
    • testing in Firefox (cert CA correctly added to Firefox’s store - lock icon says Connection Secure)
    • Nextcloud URL added to /etc/hosts file so it targets localhost while I work on it in my dev environment
    • Nothing currently accessible from outside dev environment

Summary of the issue you are facing:

Like many others before me, I am unfortunately stuck with this warning on my Overview page:

Some headers are not set correctly on your instance - The `Strict-Transport-Security` HTTP header is not set (should be at least `15552000` seconds). For enhanced security, it is recommended to enable HSTS.

My understanding is that this is almost assuredly a Traefik configuration issue but I’m stumped as to what I’m missing.

Steps to replicate it (hint: details matter!):

  1. From a cold start, stand up Docker containers using docker compose up -d; enable Nextcloud cron job

  2. In Firefox, navigate to nextcloud(dot)mydomain(dot)com, authenticate

  3. From the Nextcloud homepage, navigate to /settings/admin/overview

  4. Observe the warning message present in the “Security & setup warnings” section

Log entries

Nextcloud

None from the day of testing.

Web Browser (Firefox)

Network output upon refresh is 200’s across the board.

Console output upon refresh:

Notifications permissions granted NotificationsApp.vue:449:13
Polling interval updated to 30000 NotificationsApp.vue:412:12
Started background fetcher as session_keepalive is enabled NotificationsApp.vue:279:13
Got notification data, restoring default polling interval. NotificationsApp.vue:368:13

EDIT: adding nextcloud#####headers from Firefox’s local storage for additional info - this seems to indicate that as far as the browser is concerned, HSTS headers are present?

{
    "cache-control": "no-cache, no-store, must-revalidate",
    "content-encoding": "gzip",
    "content-length": "81",
    "content-security-policy": "default-src 'none';base-uri 'none';manifest-src 'self';frame-ancestors 'none'",
    "content-type": "application/json; charset=utf-8",
    "date": "(REDACTED)",
    "etag": "(REDACTED)",
    "feature-policy": "autoplay 'none';camera 'none';fullscreen 'none';geolocation 'none';microphone 'none';payment 'none'",
    "permissions-policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=()",
    "referrer-policy": "no-referrer",
    "strict-transport-security": "max-age=15752000; includeSubDomains; preload",
    "x-content-type-options": "nosniff",
    "x-firefox-spdy": "h2",
    "x-frame-options": "SAMEORIGIN",
    "x-nextcloud-user-status": "online",
    "x-permitted-cross-domain-policies": "none",
    "x-powered-by": "PHP/8.3.30",
    "x-request-id": "(REDACTED)",
    "x-robots-tag": "none",
    "x-user-id": "admin",
    "x-xss-protection": "1; mode=block"
}

Web server / Reverse Proxy (Traefik)

None from the day of testing.

Configuration

Nextcloud

occ config:list system:

{
    "system": {
        "htaccess.RewriteBase": "\/",
        "memcache.local": "\\OC\\Memcache\\APCu",
        "apps_paths": [
            {
                "path": "\/var\/www\/html\/apps",
                "url": "\/apps",
                "writable": false
            },
            {
                "path": "\/var\/www\/html\/custom_apps",
                "url": "\/custom_apps",
                "writable": true
            }
        ],
        "memcache.distributed": "\\OC\\Memcache\\Redis",
        "memcache.locking": "\\OC\\Memcache\\Redis",
        "redis": {
            "host": "***REMOVED SENSITIVE VALUE***",
            "password": "***REMOVED SENSITIVE VALUE***",
            "port": 6379
        },
        "trusted_proxies": "***REMOVED SENSITIVE VALUE***",
        "upgrade.disable-web": true,
        "instanceid": "***REMOVED SENSITIVE VALUE***",
        "passwordsalt": "***REMOVED SENSITIVE VALUE***",
        "secret": "***REMOVED SENSITIVE VALUE***",
        "trusted_domains": [
            "nextcloud.mydomain.com"
        ],
        "datadirectory": "***REMOVED SENSITIVE VALUE***",
        "dbtype": "mysql",
        "version": "32.0.5.0",
        "overwrite.cli.url": "https:\/\/nextcloud.mydomain.com",
        "dbname": "***REMOVED SENSITIVE VALUE***",
        "dbhost": "***REMOVED SENSITIVE VALUE***",
        "dbtableprefix": "oc_",
        "mysql.utf8mb4": true,
        "dbuser": "***REMOVED SENSITIVE VALUE***",
        "dbpassword": "***REMOVED SENSITIVE VALUE***",
        "installed": true,
        "maintenance": false,
        "maintenance_window_start": 9
    }
}

Apps

occ app:list output:

Enabled:
  - activity: 5.0.0-dev.0
  - app_api: 32.0.0
  - bruteforcesettings: 5.0.0-dev.0
  - circles: 32.0.0
  - cloud_federation_api: 1.16.0
  - comments: 1.22.0
  - contactsinteraction: 1.13.1
  - dashboard: 7.12.0
  - dav: 1.34.2
  - federatedfilesharing: 1.22.0
  - federation: 1.22.0
  - files: 2.4.0
  - files_downloadlimit: 5.0.0-dev.0
  - files_pdfviewer: 5.0.0-dev.0
  - files_reminders: 1.5.0
  - files_sharing: 1.24.1
  - files_trashbin: 1.22.0
  - files_versions: 1.25.0
  - firstrunwizard: 5.0.0-dev.0
  - logreader: 5.0.0-dev.0
  - lookup_server_connector: 1.20.0
  - nextcloud_announcements: 4.0.0-dev.0
  - notifications: 5.0.0-dev.0
  - oauth2: 1.20.0
  - password_policy: 4.0.0-dev.0
  - photos: 5.0.0-dev.1
  - privacy: 4.0.0-dev.0
  - profile: 1.1.0
  - provisioning_api: 1.22.0
  - recommendations: 5.0.0-dev.0
  - related_resources: 3.0.0-dev.0
  - serverinfo: 4.0.0-dev.0
  - settings: 1.15.1
  - sharebymail: 1.22.0
  - support: 4.0.0-dev.0
  - survey_client: 4.0.0-dev.0
  - systemtags: 1.22.0
  - text: 6.0.1
  - theming: 2.7.0
  - twofactor_backupcodes: 1.21.0
  - updatenotification: 1.22.0
  - user_status: 1.12.0
  - viewer: 5.0.0-dev.0
  - weather_status: 1.12.0
  - webhook_listeners: 1.3.0
  - workflowengine: 2.14.0
Disabled:
  - admin_audit: 1.22.0
  - encryption: 2.20.0
  - files_external: 1.24.1
  - suspicious_login: 10.0.0-dev.0
  - twofactor_nextcloud_notification: 6.0.0-dev.0
  - twofactor_totp: 14.0.0
  - user_ldap: 1.23.0

Docker configuration

Traefik stack’s docker-compose.yml (composed up first):

services:
  core_proxy:
    container_name: core_proxy
    hostname: core_proxy
    image: traefik:v3.6
    depends_on:
      - core_auth
      - core_ldap
      - core_proxy_socket
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - core_proxy
      - core_proxy_socket
    ports:
      - 80:80
      - 443:443
    volumes:
      - $DIR_APPS_CORE_PROXY/rules:/rules:ro
      - $DIR_APPS_CORE_PROXY/traefik.yml:/traefik.yml:ro
      - $DIR_LOGS/core.proxy.log:/logs/traefik.log
      - $DIR_LOGS/core.proxy.access.log:/logs/access.log
      - $DIR_CERTS:/certs
    labels:
      traefik.enable: true
      traefik.http.routers.core_proxy.rule: Host(`traefik.mydomain.com`)
      traefik.http.routers.core_proxy.tls: true
      traefik.http.routers.core_proxy.entryPoints: websecure
      traefik.http.routers.core_proxy.service: api@internal
      traefik.http.routers.core_proxy.middlewares: chain-auth-authelia@file

  core_proxy_socket:
    container_name: core_proxy_socket
    image: lscr.io/linuxserver/socket-proxy:latest
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - core_proxy_socket
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      LOG_LEVEL: info
      ALLOW_START: 0
      ALLOW_STOP: 0
      ALLOW_RESTARTS: 0
      # allowed by default
      EVENTS: 1
      PING: 1
      VERSION: 1
      # revoked by default
      AUTH: 0
      SECRETS: 0
      POST: 0
      # optional
      BUILD: 0
      COMMIT: 0
      CONFIGS: 0
      CONTAINERS: 1
      DISTRIBUTION: 0
      EXEC: 0
      IMAGES: 0
      INFO: 0
      NETWORKS: 0
      NODES: 0
      PLUGINS: 0
      SERVICES: 0
      SESSION: 0
      SWARM: 0
      SYSTEM: 0
      TASKS: 0
      VOLUMES: 0
      DISABLE_IPV6: 0

  core_auth:
    container_name: core_auth
    hostname: core_auth
    image: authelia/authelia:4.39
    depends_on:
      - core_ldap
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - core_proxy
      - core_ldap
    secrets:
      - ***REMOVED SENSITIVE VALUE(S)***
    volumes:
      - $DIR_APPS_CORE_AUTH/configuration.yml:/config/configuration.yml:ro
      - $DIR_APPS_CORE_AUTH/notification.txt:/config/notification.txt
      - $DIR_DATA_CORE_AUTH:/config/data
      - $DIR_CERTS:/config/certificates
      - $DIR_LOGS/core.auth.log:/config/authelia.log
    environment:
      AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDRESS: 'ldap://core_ldap:3890'
      AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN: 'dc=mydomain,dc=com'
      AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER: 'uid=myuid,ou=people,DC=mydomain,DC=com'
      AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: ***REMOVED SENSITIVE VALUE***
      AUTHELIA_SESSION_SECRET_FILE: ***REMOVED SENSITIVE VALUE***
      AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: ***REMOVED SENSITIVE VALUE***
      AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE: ***REMOVED SENSITIVE VALUE***
    labels:
      traefik.enable: true
      traefik.http.routers.core_auth.rule: Host(`auth.mydomain.com`)
      traefik.http.routers.core_auth.tls: true
      traefik.http.routers.core_auth.entryPoints: websecure
      traefik.http.routers.core_auth.service: core_auth
      traefik.http.routers.core_auth.middlewares: chain-auth-none@file
      traefik.http.services.core_auth.loadBalancer.server.url: http://core_auth:9091

  core_ldap:
    container_name: core_ldap
    hostname: core_ldap
    image: lldap/lldap:stable
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - core_ldap
      - core_proxy
    secrets:
      - ***REMOVED SENSITIVE VALUE(S)***
    volumes:
      - $DIR_APPS_CORE_LDAP/lldap_config.toml:/data/lldap_config.toml
      - $DIR_DATA_CORE_LDAP:/data/db
    environment:
      LLDAP_JWT_SECRET_FILE: ***REMOVED SENSITIVE VALUE***
      LLDAP_KEY_SEED_FILE: ***REMOVED SENSITIVE VALUE***
      LLDAP_LDAP_USER_PASS_FILE: ***REMOVED SENSITIVE VALUE***
    labels:
      traefik.enable: true
      traefik.http.routers.core_ldap.rule: Host(`sso.mydomain.com`)
      traefik.http.routers.core_ldap.tls: true
      traefik.http.routers.core_ldap.entryPoints: websecure
      traefik.http.routers.core_ldap.service: core_ldap
      traefik.http.routers.core_ldap.middlewares: chain-auth-none@file
      traefik.http.services.core_ldap.loadBalancer.server.url: http://core_ldap:17170

networks:
  core_ldap:
    name: core_ldap
  core_proxy:
    name: core_proxy
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.90.0/24
  core_proxy_socket:
    name: core_proxy_socket

secrets:
  ***REMOVED SENSITIVE VALUE(S)***

Nextcloud stack’s docker-compose.yml (composed up after Traefik - I’ll be using Authelia for single-sign-on in Nextcloud but I haven’t configured that yet):

services:
  cloud:
    container_name: cloud
    hostname: cloud
    image: nextcloud
    depends_on:
      - cloud_db
      - cloud_redis
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - core_proxy
      - cloud_db
      - cloud_harp
      - cloud_redis
    volumes:
      - $DIR_DATA_CLOUD:/var/www/html
    environment:
      NEXTCLOUD_HOSTNAME: nextcloud.mydomain.com
      NEXTCLOUD_TRUSTED_DOMAIN: nextcloud.mydomain.com
      TRUSTED_PROXIES: 192.168.90.0/24
      REDIS_HOST: cloud_redis
    labels:
      traefik.enable: true
      traefik.http.routers.cloud.rule: Host(`nextcloud.mydomain.com`)
      traefik.http.routers.cloud.tls: true
      traefik.http.routers.cloud.entryPoints: websecure
      traefik.http.routers.cloud.service: cloud
      traefik.http.routers.cloud.middlewares: chain-auth-authelia-cloud@file
      traefik.http.services.cloud.loadBalancer.server.url: http://cloud:80

  cloud_harp:
    container_name: cloud_harp
    hostname: cloud_harp
    image: ghcr.io/nextcloud/nextcloud-appapi-harp:release
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - cloud_harp
    secrets:
      - cloud_harp_pass
    volumes:
      - $DIR_CERTS:/certs
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      HP_SHARED_KEY_FILE: ***REMOVED SENSITIVE VALUE***
      NC_INSTANCE_URL: https://nextcloud.mydomain.com
      HP_TRUSTED_PROXY_IPS: 192.168.90.0/24
    labels:
      traefik.enable: true
      traefik.http.routers.cloud_harp.rule: Host(`nextcloud.mydomain.com`) && PathPrefix(`/exapps/`)
      traefik.http.routers.cloud_harp.entrypoints: web
      traefik.http.routers.cloud_harp.service: cloud_harp
      traefik.http.services.cloud_harp.loadBalancer.server.url: http://127.0.0.1:8780

  cloud_db:
    container_name: cloud_db
    hostname: cloud_db
    image: mariadb:lts
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - cloud_db
    secrets:
      - ***REMOVED SENSITIVE VALUE(S)***
    volumes:
      - $DIR_DATA_CLOUD_DB:/var/lib/mysql
    command: 
      --transaction-isolation=READ-COMMITTED
    environment:
      MYSQL_ROOT_PASSWORD_FILE: ***REMOVED SENSITIVE VALUE***
      MYSQL_DATABASE_FILE: ***REMOVED SENSITIVE VALUE***
      MYSQL_USER_FILE: ***REMOVED SENSITIVE VALUE***
      MYSQL_PASSWORD_FILE: ***REMOVED SENSITIVE VALUE***

  cloud_redis:
    container_name: cloud_redis
    hostname: cloud_redis
    image: redis:alpine
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - cloud_redis

networks:
  core_proxy:
    external: true
  cloud_db:
    name: cloud_db
  cloud_harp:
    name: cloud_harp
  cloud_redis:
    name: cloud_redis

secrets:
  ***REMOVED SENSITIVE VALUE(S)***

Traefik configuration

traefik.yml:

api:
  dashboard: true
  insecure: true
  debug: false
global:
  checkNewVersion: false
  sendAnonymousUsage: false
providers:
  docker:
    endpoint: tcp://core_proxy_socket:2375
    exposedByDefault: false
    network: core_proxy
  file:
    directory: /rules
    watch: true

# ---HTTP---
entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"

# ---LOGGING---
log:
  filePath: /logs/traefik.log
  level: INFO
accessLog:
  filePath: /logs/access.log
  bufferingSize: 100
  filters:
    statusCodes:
      - 204-299
      - 400-499
      - 500-599

dynamic.yml (combined, these are in a handful of files in the rules folder):

tls:
  stores:
    default:
      defaultCertificate:
        certFile: /certs/mydomain.com.crt
        keyFile: /certs/mydomain.com.key
  certificates:
    - certFile: /certs/mydomain.com.crt
      keyFile: /certs/mydomain.com.key
      stores:
        - default

http:
  middlewares:

    chain-auth-none:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-secure-headers

    chain-auth-authelia:
      chain:
        middlewares:
          - middlewares-auth-authelia
          - middlewares-rate-limit
          - middlewares-secure-headers

    chain-auth-authelia-cloud:
      chain:
        middlewares:
          - middlewares-auth-authelia
          - middlewares-rate-limit
          - middlewares-redirect-cloud
          - middlewares-secure-headers-cloud

    middlewares-auth-authelia:
      forwardAuth:
        address: "http://core_auth:9091/api/verify?rd=https://auth.mydomain.com"
        trustForwardHeader: true
        authResponseHeaders:
          - "Remote-User"
          - "Remote-Groups"

    middlewares-rate-limit:
      rateLimit:
        average: 100
        burst: 50

    middlewares-redirect-cloud:
      redirectRegex:
        permanent: true
        regex: https://(.*)/.well-known/(card|cal)dav
        replacement: https://${1}/remote.php/dav/

    middlewares-secure-headers:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        accessControlMaxAge: 100
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        stsSeconds: 63072000
        stsIncludeSubdomains: true
        stsPreload: true
        customFrameOptionsValue: SAMEORIGIN
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: same-origin
        permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=()"
        customResponseHeaders:
          X-Robots-Tag: none,noarchive,nosnippet,notranslate,noimageindex,
          server: ""

    middlewares-secure-headers-cloud:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        accessControlMaxAge: 100
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        sslRedirect: true
        stsSeconds: 15752000
        stsIncludeSubdomains: true
        stsPreload: true
        forceSTSHeader: true
        customFrameOptionsValue: SAMEORIGIN
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: no-referrer
        permissionsPolicy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=()
        customResponseHeaders:
          X-Robots-Tag: none
          server: ""

Happy to provide additional context if it would be useful o7 thanks in advance!