FreeBSD 15 使用 Nginx 搭建 HTTPS 静态文件浏览服务

在 FreeBSD 15 上使用 Nginx 替代 python3 -m http.server,配置 HTTPS、目录浏览、Basic Auth、敏感文件拦截、日志轮转、Gzip、限流、强制下载和 BBR。

在 FreeBSD 15 下,如果只是临时分享目录,python3 -m http.server 很方便;但如果要长期对外提供文件浏览和下载,Nginx 更适合。它可以直接处理 HTTPS、Basic Auth、目录浏览、访问日志、限流和安全响应头,性能和可控性都更好。

本文以 fetch.jgaga.tk 为示例域名,Web 根目录为 /usr/local/www/fetch.jgaga.tk。实际使用时,把域名和目录替换成自己的即可。

准备证书

正式对外提供 HTTPS 之前,需要先为域名签发证书。可以使用你现有的一键脚本,或使用 acme.shcertbot 等工具签发。

本文后续示例假设证书和私钥已经放在下面的位置:

/usr/local/etc/ssl/fetch.jgaga.tk/fetch.jgaga.tk.crt
/usr/local/etc/ssl/fetch.jgaga.tk/fetch.jgaga.tk.key

如果你的一键脚本生成的是其他路径,修改 Nginx 配置里的 ssl_certificatessl_certificate_key 即可。

安装并启用 Nginx

pkg install -y nginx
sysrc nginx_enable="YES"

创建 Web 根目录:

mkdir -p /usr/local/www/fetch.jgaga.tk
chown -R root:www /usr/local/www/fetch.jgaga.tk

这里推荐让目录归属为 root:www。Nginx worker 以 www 用户运行,只需要读取文件即可;后面如果要让普通用户上传文件,再单独调整目录权限。

创建认证密码文件

如果目录要对外开放,建议先加 Basic Auth。FreeBSD 自带 openssl,不需要额外安装工具。

下面以用户名 admin 为例:

printf "admin:%s\n" "$(openssl passwd -apr1)" > /usr/local/etc/nginx/.htpasswd
chown root:www /usr/local/etc/nginx/.htpasswd
chmod 640 /usr/local/etc/nginx/.htpasswd

执行第一条命令时,系统会提示输入并确认密码,输入过程不会显示字符,这是正常的。

如果要换用户名,修改冒号前面的 admin 后重新生成即可:

printf "your_username:%s\n" "$(openssl passwd -apr1)" > /usr/local/etc/nginx/.htpasswd
chown root:www /usr/local/etc/nginx/.htpasswd
chmod 640 /usr/local/etc/nginx/.htpasswd

创建日志目录

为了让日志路径和后面的 newsyslog 轮转规则一致,先显式创建 Nginx 日志目录:

mkdir -p /var/log/nginx
chown root:wheel /var/log/nginx
chmod 755 /var/log/nginx

Nginx 完整配置

编辑 /usr/local/etc/nginx/nginx.conf,可以按下面这份配置整理。它包含:

  • HTTP 自动跳转 HTTPS
  • Let’s Encrypt ACME 验证目录放行
  • TLS 1.2 / TLS 1.3
  • HTTP/2
  • 目录浏览
  • Basic Auth
  • 敏感文件拦截
  • Gzip
  • 单 IP 并发、请求频率和下载速度限制
  • 指定格式强制下载
user www;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;

    sendfile on;
    keepalive_timeout 65;

    gzip on;
    gzip_vary on;
    gzip_comp_level 5;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/x-javascript application/xml application/xml+rss application/x-sh application/x-yaml;

    limit_conn_zone $binary_remote_addr zone=addr_conn:10m;
    limit_req_zone  $binary_remote_addr zone=addr_req:10m rate=5r/s;

    server {
        listen 80;
        listen [::]:80;
        server_name fetch.jgaga.tk;

        location /.well-known/acme-challenge/ {
            root /usr/local/www/fetch.jgaga.tk;
        }

        location / {
            return 301 https://$host$request_uri;
        }
    }

    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        http2 on;
        server_name fetch.jgaga.tk;

        ssl_certificate     /usr/local/etc/ssl/fetch.jgaga.tk/fetch.jgaga.tk.crt;
        ssl_certificate_key /usr/local/etc/ssl/fetch.jgaga.tk/fetch.jgaga.tk.key;

        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 off;
        ssl_session_timeout 1d;
        ssl_session_cache shared:MozSSL:10m;
        ssl_session_tickets off;

        add_header Strict-Transport-Security "max-age=63072000" always;
        add_header X-Content-Type-Options nosniff;
        add_header X-Frame-Options SAMEORIGIN;
        add_header X-XSS-Protection "1; mode=block";

        root /usr/local/www/fetch.jgaga.tk;
        index index.html index.htm;

        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }

        location ~ \.(key|pem|conf|sh|sql)$ {
            deny all;
            access_log off;
            log_not_found off;
        }

        location / {
            autoindex on;
            autoindex_exact_size off;
            autoindex_localtime on;
            charset utf-8;

            auth_basic "Restricted Access";
            auth_basic_user_file /usr/local/etc/nginx/.htpasswd;

            limit_conn addr_conn 3;
            limit_req zone=addr_req burst=10 nodelay;
            limit_rate_after 50m;
            limit_rate 500k;
        }

        location ~* \.(jpg|jpeg|png|gif|mp4|mkv|avi|mp3|pdf|txt)$ {
            add_header Content-Disposition "attachment";
            add_header Strict-Transport-Security "max-age=63072000" always;
            add_header X-Content-Type-Options nosniff;
            add_header X-Frame-Options SAMEORIGIN;
            add_header X-XSS-Protection "1; mode=block";

            auth_basic "Restricted Access";
            auth_basic_user_file /usr/local/etc/nginx/.htpasswd;

            limit_conn addr_conn 3;
            limit_req zone=addr_req burst=10 nodelay;
            limit_rate_after 50m;
            limit_rate 500k;
        }
    }
}

注意:使用正则 location 匹配强制下载文件时,请把认证和限流也写进去。否则直接访问这些文件 URL 时,可能绕过 location / 中的控制规则。

测试并启动服务

nginx -t
service nginx start

后续修改配置后,使用 reload 即可:

nginx -t
service nginx reload

打开 https://fetch.jgaga.tk 后,应该会先弹出 Basic Auth 登录框。认证通过后,可以看到 Nginx 的目录列表。

修复点击文件 403 的权限问题

如果目录列表能显示文件名,但点击 .jpg 等文件时返回 403 Forbidden,通常是文件本身权限不足。autoindex 能列目录,不代表 Nginx 一定能读取文件内容。

推荐先把目录所有权整理为 root:www

chown -R root:www /usr/local/www/fetch.jgaga.tk

如果仍然有权限问题,再统一修正目录和文件权限:

find /usr/local/www/fetch.jgaga.tk -type d -exec chmod 755 {} \;
find /usr/local/www/fetch.jgaga.tk -type f -exec chmod 644 {} \;

执行完不需要重启 Nginx,刷新浏览器即可。

让普通用户管理文件目录

普通用户不能直接运行当前这套 Nginx 服务,因为 80 和 443 属于特权端口,证书私钥和系统日志也需要更高权限。正确做法是:Nginx 仍由 root 启动,worker 自动降权为 www;普通用户只负责上传和管理 Web 根目录里的文件。

假设普通用户是 felix,先把它加入 www 组:

pw groupmod www -m felix

再允许同组用户写入目录:

chmod -R 775 /usr/local/www/fetch.jgaga.tk
chmod g+s /usr/local/www/fetch.jgaga.tk

chmod g+s 会让目录中新建的文件继承 www 组,避免后续文件归属变得混乱。

修改用户组后,旧 SSH 会话不会自动刷新身份。重新登录,或在当前终端执行:

su - felix

然后用 id 确认输出里包含 www 组。

配置日志按天轮转

FreeBSD 不需要额外安装 logrotate,系统自带的 newsyslog 就能完成日志轮转。

创建自定义规则目录:

mkdir -p /usr/local/etc/newsyslog.conf.d/

编辑 /usr/local/etc/newsyslog.conf.d/nginx.conf

# logfilename          [owner:group]    mode count size when  flags [/pid_file]        [sig_num]
/var/log/nginx/*.log   root:wheel       644  30    * @T00  JC    /var/run/nginx.pid   30

参数说明:

  • count 30:保留最近 30 天日志。
  • @T00:每天 00:00 轮转。
  • J:使用 bzip2 压缩旧日志。
  • C:日志不存在时自动创建。
  • 30:轮转后向 Nginx master 发送 SIGUSR1,让 Nginx 重新打开日志文件,无需重启服务。

测试配置:

newsyslog -nv

如果看到类似下面的行,说明配置已被识别:

Processing /usr/local/etc/newsyslog.conf.d/nginx.conf

如果提示 /var/log/nginx/*.log 不存在,多半是 Nginx 还没产生日志,或干跑模式没有实际创建文件。只要没有语法错误即可。

强制下载指定文件类型

如果希望点击图片、视频、PDF、文本文件时不要在浏览器中预览,而是直接弹出下载,可以使用:

location ~* \.(jpg|jpeg|png|gif|mp4|mkv|avi|mp3|pdf|txt)$ {
    add_header Content-Disposition "attachment";

    auth_basic "Restricted Access";
    auth_basic_user_file /usr/local/etc/nginx/.htpasswd;

    limit_conn addr_conn 3;
    limit_req zone=addr_req burst=10 nodelay;
    limit_rate_after 50m;
    limit_rate 500k;
}

如果你的 HTTPS server 里已经有全局安全响应头,建议也在这个正则 location 中补齐一份 add_header。Nginx 的 add_header 在不同层级混用时容易出现继承行为不符合直觉的情况,直接写全更稳。

可选:内网免密、外网认证

如果以后想让内网直接访问,外网才需要密码,可以在 location / 中使用 satisfy any

location / {
    autoindex on;
    autoindex_exact_size off;
    autoindex_localtime on;
    charset utf-8;

    satisfy any;
    allow 192.168.100.0/24;
    allow 10.20.20.0/24;
    allow fd88::/64;
    allow 127.0.0.1;
    deny all;

    auth_basic "Restricted Access";
    auth_basic_user_file /usr/local/etc/nginx/.htpasswd;
}

本文完整配置没有启用这段规则。如果你没有固定内网网段需求,保持 Basic Auth 全局生效即可。

可选:配合 fail2ban 封禁限流 IP

Nginx 触发 limit_reqlimit_conn 后,会在错误日志里写入 limiting requestslimiting connections。可以用 fail2ban 继续联动封禁。

创建 /usr/local/etc/fail2ban/filter.d/nginx-limit.conf

[Definition]
failregex = ^\s*\[error\] \d+#\d+: \*\d+ limiting (requests|connections).*? client: <HOST>,
ignoreregex =

/usr/local/etc/fail2ban/jail.local 中加入:

[nginx-limit]
enabled  = true
filter   = nginx-limit
port     = http,https
logpath  = /var/log/nginx/error.log
findtime = 60
maxretry = 5
bantime  = 3600
banaction = pf

测试正则并重载:

fail2ban-regex /var/log/nginx/error.log /usr/local/etc/fail2ban/filter.d/nginx-limit.conf
fail2ban-client reload

banaction = pf 需要系统已经正确启用并配置 PF 防火墙。

可选:开启 FreeBSD BBR

FreeBSD 的 BBR 不是简单切换一个拥塞控制算法,而是切换到一个 TCP stack。它适合跨国、高延迟或存在丢包的链路,可能改善下载吞吐。

临时开启:

kldload tcp_bbr
sysctl net.inet.tcp.functions_default=bbr

验证:

sysctl net.inet.tcp.functions_default

如果输出为下面这样,就说明已经生效:

net.inet.tcp.functions_default: bbr

永久生效:

echo 'tcp_bbr_load="YES"' >> /boot/loader.conf
echo 'net.inet.tcp.functions_default=bbr' >> /etc/sysctl.conf

总结

这套配置可以把 FreeBSD 15 上的一个普通目录整理成可长期使用的 HTTPS 文件浏览服务:

  • Nginx 负责目录浏览和静态文件传输。
  • TLS 与安全响应头保证基础 HTTPS 安全性。
  • Basic Auth 防止目录裸奔。
  • 敏感文件拦截避免 .htpasswd、私钥、配置文件等被访问。
  • newsyslog 负责日志轮转。
  • Gzip 和限流分别处理传输效率与带宽保护。
  • 强制下载规则让图片、视频、PDF 等文件点击后直接下载。

完成配置后,记得每次修改都先执行:

nginx -t

确认语法无误后再 reload。