统一家里云的Web服务端口

之前很长的一段时间,我的家里云部署的服务都是一个服务监听一个端口,管理相对比较混乱一点,而且因为监听的端口较多,且服务直接暴露,安全风险会更高一些。
例如9898端口由Jellyfin监听提供服务,8848端口由Filebrowser监听提供服务,8086端口由NextCloud监听提供服务...
这样子虽然在内网访问的情况下方便管理,但是也带来了多端口,多个后端服务直接暴露,管理相对混乱。而且还有一个问题,就是Filebrowser直接监听端口,如果浏览器发送的是http请求,也就是默认在地址栏输入域名+端口时的请求,会直接返回一段文字:client sent an HTTP request to an HTTPS server,然后需要再去地址栏在http后面加上个s,就很操蛋。
而对于nginx服务器,则可以直接加上这条规则error_page 497 https://$host:8848$request_uri;即可实现自动跳转,体验要好不少。
外加上我家里云其实有专门解析的域名,可以设置不同的子域名用于区分服务,并且我因为我一年多在网站上面的实践,也有了实操经验。所以,我决定把我家里云的Web服务全部统一服务端口,通过vhost进行区分,也就是利用https的sni进行区分,顺带再做一个回落的默认页面来防止低级的IP地址扫描器,还有可能存在的运营商对用户运行的服务的扫描。

回落页面设置

首先是回落页面设置,这也就是用于掩人耳目的设置,当域名不匹配时,就会返回这个内容。
配置文件为/etc/nginx/conf.d/default.conf

    listen 8848 ssl default_server;
    listen [::]:8848 ssl default_server;

    server_name _;  # 这是一个通配符,表示任何未匹配的主机名

    ssl_certificate /home/chocola/data/certs/bt_cert.pem;
    ssl_certificate_key /home/chocola/data/certs/bt_cert.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384';

    root /var/www/; # 静态文件的位置
    index index.html; # 默认主页

    location / {
        try_files $uri $uri/ =404; # 尝试提供请求的文件或目录,如果不存在则返回404
    }

    # 处理错误页面
    error_page 404 /404.html;
    location = /404.html {
        internal;
    }
    
    error_page 497 /index.html;
    
    location = /index.html {
        internal;  # 只允许内部重定向访问
        root /var/www;
        add_header Content-Type text/html;
    }
}

记得在对应的目录放置回落页面index.html和修改证书路径位置。以及在虚拟机安装宝塔面板后,拿宝塔面板的自签名证书文件和宝塔的回落页面放到你的服务器上做回落页面,让别人误以为你是个笨蛋在用宝塔(误)

进行反代配置

安装好Nginx后,我们就可以开始配置各种服务的反代配置了,只需要确保原始IP可以正常传递,并且后端服务支持使用传递的真实IP。

Jellyfin

这官方给了详细的Nginx反代配置模板,只需要稍加改动就可以正常使用。
配置文件为/etc/nginx/conf.d/jellyfin-proxy.conf

server {
    listen 8848 ssl http2; #IPV4
    listen [::]:8848 ssl http2; #IPv6

    server_name jellyfin.nekopara.uk;
    
    error_page 497 https://$host:8848$request_uri;

    ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
    client_max_body_size 32M;

    # Comment next line to allow TLSv1.0 and TLSv1.1 if you have very old clients
    ssl_protocols TLSv1.3 TLSv1.2;

    ssl_certificate /home/chocola/data/certs/nekopara_uk.pem;
    ssl_certificate_key /home/chocola/data/certs/nekopara_uk.key;

    # use a variable to store the upstream proxy
    set $jellyfin 127.0.0.1;

    # Security / XSS Mitigation Headers
    add_header X-Content-Type-Options "nosniff";

    # Permissions policy. May cause issues with some clients
    add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always;

    # Content Security Policy
    # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
    # Enforces https content and restricts JS/CSS to origin
    # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted.
    add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; font-src 'self'";

    location / {
        # Proxy main Jellyfin traffic
        proxy_pass http://$jellyfin:8096;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Protocol $scheme;
        proxy_set_header X-Forwarded-Host $http_host;

        # Disable buffering when the nginx proxy gets very resource heavy upon streaming
        proxy_buffering off;
    }

    location /socket {
        # Proxy Jellyfin Websockets traffic
        proxy_pass http://$jellyfin:8096;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Protocol $scheme;
        proxy_set_header X-Forwarded-Host $http_host;
    }
}

Filebrowser

对于这个应用,官方没有给Nginx的反代配置,于是我自己写一个:
配置文件为/etc/nginx/conf.d/filebrowser-proxy.conf

server {
    listen 8848 ssl http2;
    listen [::]:8848 ssl http2;

    server_name file.nekopara.uk nas.nekopara.uk;

    # 自动跳转 497 错误
    error_page 497 https://$host:8848$request_uri;

    ssl_certificate /home/chocola/data/certs/nekopara_uk.pem;
    ssl_certificate_key /home/chocola/data/certs/nekopara_uk.key;

    # 优化后的 SSL 配置
    ssl_session_timeout 1d;               # 延长会话时间,提高性能
    ssl_session_cache shared:SSL:10m;    # 启用 SSL 缓存
    ssl_session_tickets off;

    # 现代安全协议:只允许 TLS 1.2 和 1.3
    ssl_protocols TLSv1.2 TLSv1.3;

    # 使用高性能且安全的加密套件
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    
    ssl_prefer_server_ciphers on; 

    location / {
        client_max_body_size 512m;
        
        # 关键优化:Filebrowser 传输大文件或流媒体时,缓冲区过小会导致中断
        proxy_buffering off; 
        proxy_request_buffering off;
        
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # 根据 server_name 转发到不同端口(或者分两个 server 块写)
        if ($host = "file.nekopara.uk") {
            proxy_pass http://127.0.0.1:8001;
        }
        if ($host = "nas.nekopara.uk") {
            proxy_pass http://127.0.0.1:8002;
        }
    }
}

Filebrowser在配置的过程中踩了一个大坑,因为其他服务都开了http2,一开始Filebrowser的配置文件没开,因为我就沿用了我云服务器上面的配置文件,结果浏览器加载报错:NS_ERROR_NET_PARTIAL_TRANSFERNS_ERROR_NET_INTERRUPT。对应的修复方式是添加这两个参数解决:

  • proxy_buffering off;
  • proxy_request_buffering off;

这个问题折磨了我好久,并且是我配置完成NextCloud后才发现的爆雷。

qBittorrent

虽然官方给了Nginx反代的配置文件,但是也遇到了坑:反代后打开页面空白,什么都没有。
经过排查是需要把 启用 Host header 属性验证 这个选项关掉,具体位置在:
选项 > Web UI > Web 用户界面(远程控制) > 验证 > 启用 Host header 属性验证
关掉后就正常了。
配置文件我就用了官方的模板,反代的配置文件为/etc/nginx/conf.d/qbittorrent-proxy.conf

server {
   listen 8848 ssl;

   server_name  qb.nekopara.fun;
   error_page 497 https://$host:8848$request_uri;

   # 修改为你的ssl证书位置
   ssl_certificate /home/chocola/data/certs/nekopara_uk.pem;
   ssl_certificate_key /home/chocola/data/certs/nekopara_uk.key;
   ssl_session_timeout 5m;
   ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
   ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
   ssl_prefer_server_ciphers on;

    location / {
        proxy_pass         http://127.0.0.1:8003/;
        proxy_http_version 1.1;

        # headers recognized by qBittorrent
        proxy_set_header   Host               $proxy_host;
        proxy_set_header   X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host   $http_host;
        proxy_set_header   X-Forwarded-Proto  $scheme;

        # optionally, you can adjust the POST request size limit, to allow adding a lot of torrents at once
        #client_max_body_size 100M;

        # No longer required since qBittorrent v5.1:
        # Since v4.2.2, is possible to configure qBittorrent
        # to set the "Secure" flag for the session cookie automatically.
        # However, that option does nothing unless using qBittorrent's built-in HTTPS functionality.
        # For this use case, where qBittorrent itself is using plain HTTP
        # (and regardless of whether or not the external website uses HTTPS),
        # the flag must be set here, in the proxy configuration itself.
        # Note: If this flag is set while the external website uses only HTTP, this will cause
        # the login mechanism to not work without any apparent errors in console/network resulting in "auth loops".
        proxy_cookie_path  /                  "/; Secure";
    }
}

Web服务器迁移

因为本身这个服务器上面也运行有两个传统的网站架构应用,需要把Web服务器从Apache换成Nginx。

NextCloud

NextCloud本身在我家里云早就存在,甚至可以说我配置这个家里云很大程度就是为了用NextCloud。但是之前部署用的是Apache作为Web服务器而不是Nginx。现在因为需要Nginx在前面做一层反代,自然没必要运行两个独立的Web服务器,于是就考虑让Nginx来负责NextCloud的流量。而且Nginx相比于Apache也要更加轻量。
对于NextCloud,官方给的配置文件就可以直接用,但是存在一个严重的问题:上传速度极慢!对应的问题是配置中设置:
fastcgi_request_buffering on;
改成:
fastcgi_request_buffering off;
问题解决,原因是禁用 FastCGI 请求缓冲可避免大文件上传时内存堆积,提升流式上传性能。
扫除障碍后,NextCloud也平滑切换。
配置文件为/etc/nginx/conf.d/nextcloud.conf

# Version 2024-07-17

upstream php-handler {
    server 127.0.0.1:10082;
    #server unix:/run/php/php8.2-fpm.sock;
}

# Set the `immutable` cache control options only for assets with a cache busting `v` argument
map $arg_v $asset_immutable {
    "" "";
    default ", immutable";
}

server {
    listen 8848 ssl http2; #IPV4
    listen [::]:8848 ssl http2; #IPv6

    server_name nextcloud.nekopara.uk;
    error_page 497 https://$host:8848$request_uri;

    # Path to the root of your installation
    root /opt/nextcloud;

    # Use Mozilla's guidelines for SSL/TLS settings
    # https://mozilla.github.io/server-side-tls/ssl-config-generator/
    ssl_certificate     /home/chocola/data/certs/nekopara_uk.pem;
    ssl_certificate_key /home/chocola/data/certs/nekopara_uk.key;

    # Prevent nginx HTTP Server Detection
    server_tokens off;

    # HSTS settings
    # WARNING: Only add the preload option once you read about
    # the consequences in https://hstspreload.org/. This option
    # will add the domain to a hardcoded list that is shipped
    # in all major browsers and getting removed from this list
    # could take several months.
    #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload" always;

    # set max upload size and increase upload timeout:
    client_max_body_size 512M;
    client_body_timeout 300s;
    fastcgi_buffers 64 4K;

    # Enable gzip but do not remove ETag headers
    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;

    # Pagespeed is not supported by Nextcloud, so if your server is built
    # with the `ngx_pagespeed` module, uncomment this line to disable it.
    #pagespeed off;

    # The settings allows you to optimize the HTTP2 bandwidth.
    # See https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/
    # for tuning hints
    client_body_buffer_size 512k;

    # HTTP response headers borrowed from Nextcloud `.htaccess`
    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;

    # Remove X-Powered-By, which is an information leak
    fastcgi_hide_header X-Powered-By;

    # Set .mjs and .wasm MIME types
    # Either include it in the default mime.types list
    # and include that list explicitly or add the file extension
    # only for Nextcloud like below:
    include mime.types;
    types {
        text/javascript mjs;
        application/wasm wasm;
    }

    # Specify how to handle directories -- specifying `/index.php$request_uri`
    # here as the fallback means that Nginx always exhibits the desired behaviour
    # when a client requests a path that corresponds to a directory that exists
    # on the server. In particular, if that directory contains an index.php file,
    # that file is correctly served; if it doesn't, then the request is passed to
    # the front-end controller. This consistent behaviour means that we don't need
    # to specify custom rules for certain paths (e.g. images and other assets,
    # `/updater`, `/ocs-provider`), and thus
    # `try_files $uri $uri/ /index.php$request_uri`
    # always provides the desired behaviour.
    index index.php index.html /index.php$request_uri;

    # Rule borrowed from `.htaccess` to handle Microsoft DAV clients
    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;
    }

    # Make a regex exception for `/.well-known` so that clients can still
    # access it despite the existence of the regex rule
    # `location ~ /(\.|autotest|...)` which would otherwise handle requests
    #/var/www/nextcloud for `/.well-known`.
    location ^~ /.well-known {
        # The rules in this block are an adaptation of the rules
        # in `.htaccess` that concern `/.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; }

        # Let Nextcloud's API for `/.well-known` URIs handle all other
        # requests by passing them to the front-end controller.
        return 301 /index.php$request_uri;
    }

    # Rules borrowed from `.htaccess` to hide certain paths from clients
    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)  { return 404; }
    location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console)                { return 404; }

    # Ensure this block, which passes PHP files to the PHP process, is above the blocks
    # which handle static assets (as seen below). If this block is not declared first,
    # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php`
    # to the URI, resulting in a HTTP 500 error response.
    location ~ \.php(?:$|/) {
        # Required for legacy support
        rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/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;                   # Required as PHP-FPM does not support chunked transfer encoding and requires a valid ContentLength header.

        fastcgi_max_temp_file_size 0;
    }

    # Serve static files
    location ~ \.(?:css|js|mjs|svg|gif|ico|jpg|png|webp|wasm|tflite|map|ogg|flac|mp4|webm)$ {
        try_files $uri /index.php$request_uri;
        # HTTP response headers borrowed from Nextcloud `.htaccess`
        add_header Cache-Control                     "public, max-age=15778463$asset_immutable";
        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;
        access_log off;     # Optional: Don't log access to assets
    }

    location ~ \.(otf|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
    }

    # Rule borrowed from `.htaccess`
    location /remote {
        return 301 /remote.php$request_uri;
    }

    location / {
        try_files $uri $uri/ /index.php$request_uri;
    }
}

Typecho

对,没错,虽然我博客现在已经迁移上云了,但是本地还是运行有一份独立的副本的。这主要是作为备份用途,平时也不会有访问流量,但是当云服务器遇到灾难性问题,本地仍然有一份可靠的副本。每写一篇新的文章发布,我都会用家里云从云服务器拉取最新的网站数据。同时这份副本也是做好了随时提供服务的准备的:配置好了Cloudflare的源服务器证书,随时可以通过Cloudflare切换解析套Cloudflare接管流量。
当然,因为这只是作为备份和应急,不需要这么多防护规则,所以配置文件相对也简单得多。
配置文件为/etc/nginx/conf.d/typecho.conf

server {
    listen 8848 ssl http2;
    server_name www.nekopara.uk;

    # SSL certificate and key paths
    ssl_certificate /home/chocola/data/certs/cloudflare_certs/nekopara_uk.pem;
    ssl_certificate_key /home/chocola/data/certs/cloudflare_certs/nekopara_uk.key;

    # SSL settings (optional but recommended for security)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Root directory of the website
    root /home/typecho;
    index index.html index.php;

    client_max_body_size 16M;

    # Serve static files directly
    location / {
        try_files $uri $uri/ /index.html /index.php?$args;
    }

    # PHP processing
    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:10082;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to hidden files
    location ~ /\.ht {
        deny all;
    }
}

完工验收

当确认通过新的域名+端口访问服务都没问题后,就可以优雅地把防火墙之前开放的各种端口都关闭,并且停止Apache的进程。
以及确保这些被反代的服务端关闭HTTPS监听,只监听本地回环127.0.0.1的HTTP请求。因为如果本地回环还是TLS的话会造成不必要的性能损耗,且本地回环足够安全,也没有TLS加密的必要,TLS终止于Nginx就可以了。
至此,家里云服务架构优化完美收工。