Hi everyone,
Iām building up my home lab, especially with NC and Iām facing problems with āContent-Security-Policyā in my browser.
I know this topic has already been discussed several times but I havenāt found a solution to my problem after reading posts on this subject and try proposed solutions.
The issue Iām facing
I canāt install any applications from the WebUI.
It throws a message:
This app cannot be enabled because it makes the server unstable
My browser console returns :
Refused to connect to āhttp://cloud.domain.tld/apps/files/ā because it violates the following Content Security Policy directive: āconnect-src āselfāā.
Config overview
- OS : Debian GNU/Linux 12 (bookworm)
- Iām using Traefik as reverse proxy (image traefik:2.11.2) which handles SSL termination
- Iām using Nextcloud (image nextcloud:29.0.0-fpm-alpine)
- Iām using Nginx as web-server (image nginx:1.26.0-alpine)
- Iām using Postgres as database (image postgres:16.3-alpine)
- Iām using Redis (image redis:7.2.4-alpine)
Docker-compose
As I said, I use Ansible for deployment. Iāve ātranslatedā it into docker-compose to make it easier to understand for people who arenāt used to using Ansible.
version: '3'
services:
db_nextcloud:
image: 'postgres:16.3-alpine'
restart: 'unless-stopped'
volumes:
- "~/docker/nextcloud/postgres/init/:/docker-entrypoint-initdb.d/"
environment:
- POSTGRES_USER = '<postgres_user>'
- POSTGRES_PASSWORD = '<postgres_pwd>'
- POSTGRES_DB = 'nextcloud'
networks:
- nextcloud_net
- db_net
redis_nextcloud:
image: 'redis:7.2.4-alpine'
restart: 'unless-stopped'
...
networks:
- nextcloud_net
nginx_nextcloud:
image: 'nginx:1.26.0-alpine'
restart: 'unless-stopped'
volumes:
- ~/docker/nextcloud/nginx/conf:/etc/nginx/conf.d/
- ~/docker/nextcloud/nextcloud/html:/var/www/html:ro
networks:
- traefik_proxy
- nextcloud_net
nextcloud:
image: 'nextcloud:29.0.0-fpm-alpine'
restart: 'unless-stopped'
volumes:
- /etc/localtime:/etc/localtime:ro
- ~/docker/nextcloud/nextcloud/html:/var/www/html
environment:
- NEXTCLOUD_ADMIN_USER = '<user.name>'
- NEXTCLOUD_ADMIN_PASSWORD = '<user.pass>'
- POSTGRES_HOST = 'db_nextcloud'
- POSTGRES_DB = 'nextcloud'
- POSTGRES_USER = '<postgres_user>'
- POSTGRES_PASSWORD = '<postgres_pwd>'
- REDIS_HOST = 'redis_nextcloud'
- REDIS_HOST_PORT = '<redis.port>'
- SMTP_HOST = '<smtp.server>'
- SMTP_PORT = '<smtp.port>'
- SMTP_AUTHTYPE = LOGIN
- SMTP_NAME = '<smtp.user>'
- SMTP_PASSWORD = '<smtp.pass>'
- TRUSTED_PROXIES = '172.18.0.254' # Address of traefik docker
- NEXTCLOUD_TRUSTED_DOMAINS = 'cloud.domain.tld'
- OVERWRITEPROTOCOL = 'https'
- OVERWRITECLIURL = 'https://cloud.domain.tld'
- OVERWRITEHOST = 'cloud.domain.tld'
networks:
- traefik_proxy
- nextcloud_net
Traefik static configuration
api=true
log=true
log.level=DEBUG
accessLog=true
accessLog.filePath=/traefik.log
accessLog.bufferingSize=100
providers.docker=true
providers.docker.endpoint=unix:///var/run/docker.sock
providers.docker.exposedByDefault=false
providers.docker.network=traefik_proxy
providers.file.directory=/rules
providers.file.watch=true
entryPoints.http.address=:80
entryPoints.https.address=:443
entrypoints.https.http.tls.certresolver=letsencrypt
certificatesResolvers.letsencrypt.acme.email='<user.mail>'
certificatesResolvers.letsencrypt.acme.storage=/acme.json
certificatesresolvers.letsencrypt.acme.tlschallenge=true
Traefik dynamic configuration
I set up dynamic configuration with docker labels. To make things easier to understand, Iāve gathered the relevant parameters in a TOML file.
[http.routers]
[http.routers.http-catchall]
entryPoints = ["http"]
rule = "HostRegexp(`{host:.+}`)"
middlewares = ["redirect-to-https"]
[http.routers.nextcloud-rtr]
entryPoints = ["http", "https"]
rule = "Host(`cloud.domain.tld`)"
service = "nextcloud-svc"
middlewares = ["nextcloud-chain"]
[http.routers.nextcloud-rtr.tls]
certResolver = "letsencrypt"
[http.middlewares]
[http.middlewares.redirect-to-https.redirectScheme]
scheme = "https"
[http.middlewares.nextcloud-chain]
[http.middlewares.nextcloud-chain.chain]
middlewares = [ "chain-authelia", "middlewares-redirectDAV"]
[http.middlewares.chain-no-auth]
[http.middlewares.chain-no-auth.chain]
middlewares = [ "middlewares-rate-limit", "middlewares-secure-headers"]
[http.middlewares.chain-authelia]
[http.middlewares.chain-authelia.chain]
middlewares = [ "middlewares-rate-limit", "middlewares-secure-headers", "middlewares-authelia"]
[http.middlewares.middlewares-rate-limit]
[http.middlewares.middlewares-rate-limit.rateLimit]
average = 100
burst = 50
[http.middlewares.middlewares-secure-headers]
[http.middlewares.middlewares-secure-headers.headers]
accessControlAllowMethods= ["GET", "OPTIONS", "PUT"]
accessControlMaxAge = 100
hostsProxyHeaders = ["X-Forwarded-Host"]
sslRedirect = true
stsSeconds = 63072000
stsIncludeSubdomains = true
stsPreload = true
forceSTSHeader = true
customFrameOptionsValue = "SAMEORIGIN"
contentTypeNosniff = true
browserXssFilter = true
referrerPolicy = "same-origin"
permissionsPolicy = "camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
[http.middlewares.middlewares-secure-headers.headers.customResponseHeaders]
X-Robots-Tag = "none,noarchive,nosnippet,notranslate,noimageindex,"
server = ""
[http.middlewares.middlewares-authelia]
[http.middlewares.middlewares-authelia.forwardAuth]
address = "http://<authelia_host>:<authelia_port>/api/authz/forward-auth"
trustForwardHeader = true
authResponseHeaders = ["Remote-User", "Remote-Groups"]
[http.middlewares.middlewares-redirectDAV]
[http.middlewares.middlewares-redirectDAV.redirectRegex]
regex = "https://(.*)/.well-known/(?:card|cal)dav"
replacement = "https://$${1}/remote.php/dav"
permanent = true
[http.services]
[http.services.nextcloud-svc.loadBalancer]
[http.services.nextcloud-svc.loadBalancer.servers]
url = "http://<nginx_ip>:80"
Nginx configuration
I took the nginx.conf file that is provided with the docker image
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'"$http_x_forwarded_proto" "$http_x_forwarded_host"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
map $arg_v $asset_immutable {
"" "";
default "immutable";
}
upstream php-handler {
server <nextcloud_host>:9000;
}
server {
listen 80;
client_max_body_size 512M;
client_body_timeout 300s;
fastcgi_buffers 64 4K;
client_body_buffer_size 512k;
gzip on;
gzip_vary on;
gzip_comp_level 4;
gzip_min_length 256;
gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
gzip_types application/atom+xml text/javascript application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "noindex, nofollow" always;
add_header X-XSS-Protection "1; mode=block" always;
fastcgi_hide_header X-Powered-By;
root /var/www/html;
index index.php index.html /index.php$request_uri;
location = / {
if ( $http_user_agent ~ ^DavClnt ) {
return 302 /remote.php/webdav/$is_args$args;
}
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location ^~ /.well-known {
location = /.well-known/carddav { return 301 /remote.php/dav/; }
location = /.well-known/caldav { return 301 /remote.php/dav/; }
location /.well-known/acme-challenge { try_files $uri $uri/ =404; }
location /.well-known/pki-validation { try_files $uri $uri/ =404; }
return 301 /index.php$request_uri;
}
location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; }
location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; }
location ~ \.php(?:$|/) {
rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
fastcgi_param HTTPS on;
fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice
fastcgi_param front_controller_active true; # Enable pretty urls
fastcgi_pass php-handler;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_max_temp_file_size 0;
}
location ~* \.(?:js|mjs|map)$ {
types {
text/javascript js mjs;
application/json map;
}
try_files $uri /index.php$request_uri;
add_header Cache-Control "public, max-age=15778463, $asset_immutable";
access_log off;
}
location ~ \.(?:css|svg|gif|png|jpg|ico|wasm|tflite|map|ogg|flac)$ {
try_files $uri /index.php$request_uri;
add_header Cache-Control "public, max-age=15778463, $asset_immutable";
access_log off; # Optional: Don't log access to assets
location ~ \.wasm$ {
default_type application/wasm;
}
}
location ~ \.woff2?$ {
try_files $uri /index.php$request_uri;
expires 7d; # Cache-Control policy borrowed from `.htaccess`
access_log off; # Optional: Don't log access to assets
}
location /remote {
return 301 /remote.php$request_uri;
}
location / {
try_files $uri $uri/ /index.php$request_uri;
}
}
}
Nextcloud configuration
<?php
$CONFIG = array (
'memcache.local' => '\\OC\\Memcache\\APCu',
'apps_paths' =>
array (
0 =>
array (
'path' => '/var/www/html/apps',
'url' => '/apps',
'writable' => false,
),
1 =>
array (
'path' => '/var/www/html/custom_apps',
'url' => '/custom_apps',
'writable' => true,
),
),
'memcache.distributed' => '\\OC\\Memcache\\Redis',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'redis' =>
array (
'host' => '<redis_host>',
'password' => '<redis_pwd>',
'port' => <redis_port>,
),
'overwriteprotocol' => 'https', # <- tried to remove
'overwrite.cli.url' => 'https://cloud.domain.tld', # <- tried to remove
'overwritewebroot' => '/', # <- tried to remove
'overwritehost' => 'cloud.domain.tld', # <- tried to remove
'trusted_proxies' =>
array (
0 => '172.18.0.254',
),
'upgrade.disable-web' => true,
'passwordsalt' => '<passwordsalt>',
'secret' => '<secret>',
'trusted_domains' =>
array (
0 => 'localhost',
1 => 'cloud.domain.tld',
),
'datadirectory' => '/var/www/html/data',
'dbtype' => 'pgsql',
'version' => '29.0.0.19',
'dbname' => 'nextcloud',
'dbhost' => 'db_nextcloud',
'dbport' => '',
'dbtableprefix' => 'oc_',
'dbuser' => 'oc_<postgres_user>',
'dbpassword' => '<postgres_pwd>',
'installed' => true,
'instanceid' => '<instanceid>',
);
Relevant logs
Nginx logs
2024/05/15 13:13:34 [notice] 55#55: *1182 "^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/proxy)" does not match "/index.php/settings/apps/enable", client: 172.18.0.254, server: , request: "POST /settings/apps/enable HTTP/1.1", host: "cloud.domain.tld", referrer: "https://cloud.domain.tld/settings/apps/featured/calendar"
172.18.0.254 - - [15/May/2024:13:13:34 +0200] "POST /settings/apps/enable HTTP/1.1" 200 52 "https://cloud.domain.tld/settings/apps/featured/calendar" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" "<my_ip>" "https" "cloud.domain.tld"
172.18.0.254 - - [15/May/2024:13:13:34 +0200] "GET /apps/files HTTP/1.1" 301 169 "https://cloud.domain.tld/settings/apps/featured/calendar" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" "<my_ip>" "https" "cloud.domain.tld"
2024/05/15 13:13:34 [notice] 58#58: *1205 "^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/proxy)" does not match "/ocs/v2.php/core/navigation/apps", client: 172.18.0.254, server: , request: "GET /ocs/v2.php/core/navigation/apps?format=json HTTP/1.1", host: "cloud.domain.tld", referrer: "https://cloud.domain.tld/settings/apps/featured/calendar"
2024/05/15 13:13:34 [notice] 55#55: *1182 "^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode\/proxy)" does not match "/index.php/settings/apps/disable", client: 172.18.0.254, server: , request: "POST /settings/apps/disable HTTP/1.1", host: "cloud.domain.tld", referrer: "https://cloud.domain.tld/settings/apps/featured/calendar"
172.18.0.254 - - [15/May/2024:13:13:35 +0200] "POST /settings/apps/disable HTTP/1.1" 200 22 "https://cloud.domain.tld/settings/apps/featured/calendar" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" "<my_ip>" "https" "cloud.domain.tld"
172.18.0.254 - - [15/May/2024:13:13:35 +0200] "GET /ocs/v2.php/core/navigation/apps?format=json HTTP/1.1" 200 338 "https://cloud.domain.tld/settings/apps/featured/calendar" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" "<my_ip>" "https" "cloud.domain.tld"
Traefik logs
<my_ip> - - [15/May/2024:11:20:54 +0000] "POST /settings/apps/enable HTTP/2.0" 200 52 "-" "-" 25739 "https-nextcloud-rtr@docker" "http://<nginx_ip>:80" 96ms
<my_ip> - - [15/May/2024:11:20:54 +0000] "GET /apps/files HTTP/2.0" 301 169 "-" "-" 25740 "https-nextcloud-rtr@docker" "http://<nginx_ip>:80" 8ms
<my_ip> - - [15/May/2024:11:20:54 +0000] "POST /settings/apps/disable HTTP/2.0" 200 22 "-" "-" 25741 "https-nextcloud-rtr@docker" "http://<nginx_ip>:80" 210ms
<my_ip> - - [15/May/2024:11:20:54 +0000] "GET /ocs/v2.php/core/navigation/apps?format=json HTTP/2.0" 200 338 "-" "-" 25742 "https-nextcloud-rtr@docker" "http://<nginx_ip>:80" 257ms
What Iāve tried :
- Switch from chain-authelia middleware to chain-no-auth middleware in Traefik dynamic configuration ā No changes
[http.middlewares.nextcloud-chain]
[http.middlewares.nextcloud-chain.chain]
middlewares = [ "chain-no-auth", "middlewares-redirectDAV"]
- Add custom request header in traefik dynamic configuration ā No changes
[http.middlewares.middlewares-secure-headers.headers.customrequestheaders]
X-Forwarded-Proto = "https"
- Install apps through occ command ā Apps are installed properly AFAIK So the issue seems to come from how the WebUI sends the request.
- Play with the overwrite* parameters in the nextcloud config.php file ā No changes
- Downgrate Nextcloud to an older version (28.0.5-fpm-alpine) ā No changes
What I notice
- The address in the CSP error message uses HTTP instead of HTTPS even if HTTPS is forced through
'overwriteprotocol' => 'https'
- When I try to access http://cloud.domain.tld/apps/files/ in my browser. Traefik redirects me to https://cloud.domain.tld/apps/files/ and Iām able to access the file page
- I have several apps that are running properly behind Traefik