自托管文件服务器:设计与部署指南

在 Ubuntu 上使用 nginx 搭建自托管文件服务器的实用指南,适用于局域网共享和受控的公网暴露。


1. 架构设计

1.1 目录结构


/srv/fileserver/
├── data/                    # 所有用户上传的文件存放于此
│   ├── public/              # 公开可读(无需认证)
│   ├── shared/              # 局域网认证用户
│   ├── private/             # 仅限所有者
│   └── temp/                # 暂存区,每小时自动清理
├── logs/                    # 访问日志 + 错误日志
│   ├── access.log
│   └── error.log
├── config/
│   ├── nginx.conf           # nginx 站点配置片段
│   └── htpasswd             # Basic 认证凭据(shared/ 区域)
├── uploads/                 # PHP / Python 上传处理程序
│   └── upload.php
└── scripts/
    ├── cleanup.sh           # 清理 /temp/ 目录下超过 1 小时的文件
    └── stats.sh             # 每日使用报告

1.2 权限模型


# 创建目录树
sudo mkdir -p /srv/fileserver/{data/{public,shared,private,temp},logs,config,uploads,scripts}

# 所有者:Web 服务器用户(Ubuntu 中为 www-data)
sudo chown -R www-data:www-data /srv/fileserver

# 访问规则
sudo chmod 755 /srv/fileserver/data/public # 所有人可列出和读取
sudo chmod 750 /srv/fileserver/data/shared # 组可读(htpasswd 用户)
sudo chmod 700 /srv/fileserver/data/private # 仅限所有者
sudo chmod 770 /srv/fileserver/data/temp # 上传处理程序可写

# 日志 — 仅管理员可读
sudo chmod 640 /srv/fileserver/logs/*

1.3 技术栈

层次 选择 理由
Web 服务器 nginx 轻量、久经考验的反向代理
上传处理程序 PHP-FPM 或 Python Flask 轻松处理 multipart 上传
认证 Basic 认证 + IP 白名单 简单,无需 session cookie
HTTPS Let's Encrypt (certbot) 免费、自动续期
监控 GoAccess 基于日志的实时访问仪表盘

2. nginx 反向代理配置

2.1 完整 /etc/nginx/sites-available/fileserver


# /etc/nginx/sites-available/fileserver

# --- 速率限制区域 ---
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=download:10m rate=20r/s;

# --- 每 IP 连接数限制 ---
limit_conn_zone $binary_remote_addr zone=connperip:10m;
limit_conn connperip 10;

server {
listen 80;
server_name files.mydomain.com;

# 生产环境中重定向到 HTTPS
# return 301 https://$host$request_uri;

root /srv/fileserver/data;
index index.html index.htm;

# --- 带缓存的静态文件服务 ---
location /public/ {
alias /srv/fileserver/data/public/;
autoindex on; # 目录列表
autoindex_exact_size off; # 显示人类可读的大小
autoindex_localtime on;

# 浏览器缓存头
expires 7d;
add_header Cache-Control "public, immutable";

# 安全头
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
}

# --- 受保护的共享区域(Basic 认证) ---
location /shared/ {
alias /srv/fileserver/data/shared/;
autoindex on;
autoindex_exact_size off;

auth_basic "Restricted Access";
auth_basic_user_file /srv/fileserver/config/htpasswd;

# 仅局域网客户端可跳过认证
satisfy any;
allow 192.168.0.0/16;
allow 10.0.0.0/8;
deny all;
}

# --- 私有区域(仅限所有者) ---
location /private/ {
alias /srv/fileserver/data/private/;
autoindex on;

# 仅限 localhost 访问;使用 SSH 隧道进行远程访问
allow 127.0.0.1;
allow ::1;
deny all;
}

# --- 上传端点(代理到 Python/FastCGI) ---
location /upload {
limit_req zone=upload burst=3 nodelay;

# 限制上传大小
client_max_body_size 500M;
client_body_timeout 300s;

# 代理到内部上传服务(例如 Flask 监听 :5000)
proxy_pass http://127.0.0.1:5000;
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_read_timeout 300s;
}

# --- 带速率限制的下载 ---
location /download/ {
alias /srv/fileserver/data/public/;
limit_req zone=download burst=10 nodelay;
limit_rate 2M; # 每连接 2 MB/s
}

# --- 日志 ---
access_log /srv/fileserver/logs/access.log combined;
error_log /srv/fileserver/logs/error.log warn;
}

2.2 启用站点


sudo ln -sf /etc/nginx/sites-available/fileserver /etc/nginx/sites-enabled/
sudo nginx -t          # 测试语法
sudo systemctl reload nginx


3. 上传/下载限制

3.1 文件大小限制


# 在 server {} 或 location {} 块中
client_max_body_size       2G;    # 全局最大值
client_body_buffer_size    128k;  # 写入磁盘前的缓冲区大小
client_body_timeout        600s;  # 慢速客户端的超时时间

3.2 文件扩展名白名单(Python Flask 示例)


# /srv/fileserver/uploads/upload.py
import os
from flask import Flask, request, jsonify
from werkzeug.utils import secure_filename

app = Flask(__name__)

ALLOWED_EXTENSIONS = {
# 文档
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'md',
# 图片
'jpg', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'webp', 'tiff',
# 压缩包
'zip', 'tar', 'gz', 'bz2', 'xz', '7z', 'rar',
# 代码/数据
'v', 'sv', 'vhd', 'vhdl', 'bit', 'bin', 'hex', 'json', 'xml', 'yaml',
# 媒体
'mp4', 'mkv', 'avi', 'mov', 'mp3', 'wav', 'flac',
}

BLOCKED_EXTENSIONS = {
'sh', 'bash', 'exe', 'com', 'bat', 'ps1', 'py', 'pl', 'rb',
'php', 'jsp', 'asp', 'cgi',
}

MAX_FILE_SIZE = 500 * 1024 * 1024 # 500 MB
UPLOAD_FOLDER = '/srv/fileserver/data/temp'

def allowed_file(filename: str) -> bool:
if '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
if ext in BLOCKED_EXTENSIONS:
return False
return ext in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400

file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400

if not allowed_file(file.filename):
return jsonify({'error': 'File type not allowed'}), 415

# 先检查 Content-Length,再读取文件内容
if request.content_length and request.content_length > MAX_FILE_SIZE:
return jsonify({'error': 'File too large'}), 413

filename = secure_filename(file.filename)
file.save(os.path.join(UPLOAD_FOLDER, filename))
return jsonify({'message': f'Uploaded {filename}'}), 201

if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)

3.3 速率限制总结

区域 速率 突发 用途
upload 5 r/s 3 防止上传洪水攻击
download 20 r/s 10 公平带宽共享
connperip 10 连接 - 防止连接耗尽

4. 访问日志与安全审计

4.1 nginx 日志格式


# 添加到 /etc/nginx/nginx.conf 的 http{} 块中,用于详细审计
log_format audit '$remote_addr - $remote_user [$time_local] '
                 '"$request" $status $body_bytes_sent '
                 '"$http_referer" "$http_user_agent" '
                 'rt=$request_time uct="$upstream_connect_time" '
                 'uht="$upstream_header_time" urt="$upstream_response_time" '
                 'pipe=$pipe connection=$connection '
                 'request_length=$request_length '
                 'gzip_ratio=$gzip_ratio';

access_log /srv/fileserver/logs/access.log audit;

4.2 使用 GoAccess 进行实时监控


# 安装
sudo apt install goaccess

# 终端仪表盘
goaccess /srv/fileserver/logs/access.log --log-format=COMBINED

# 生成静态 HTML 报告(cron 每 5 分钟执行一次)
goaccess /srv/fileserver/logs/access.log \
--log-format=COMBINED \
-o /srv/fileserver/data/public/report.html \
--real-time-html

4.3 审计检查清单(每周)


#!/bin/bash
# /srv/fileserver/scripts/audit.sh

LOG=/srv/fileserver/logs/access.log
echo "=== Top 10 IPs by requests ==="
awk '{print $1}' "$LOG" | sort | uniq -c | sort -rn | head -10

echo "=== 4xx/5xx errors today ==="
grep "$(date +%d/%b/%Y)" "$LOG" | awk '$9 ~ /^[45]/ {print $1, $7, $9}' | tail -20

echo "=== Large uploads (>100 MB) ==="
awk '$10 > 104857600 {print $1, $7, $10/1048576 "MB"}' "$LOG"

echo "=== Suspicious extensions accessed ==="
grep -iE '\.(sh|exe|php|bak|sql|swp)$' "$LOG"

4.4 fail2ban 集成


# /etc/fail2ban/filter.d/nginx-fileserver.conf
[Definition]
failregex = ^<HOST> -.* "(GET|POST).*(\.sh|\.exe|\.php|\.env|wp-admin).*" 40[034]
            ^<HOST> -.* "(GET|POST).*/\.git/.*" 40[034]
ignoreregex =

# /etc/fail2ban/jail.local
[nginx-fileserver]
enabled = true
port = http,https
filter = nginx-fileserver
logpath = /srv/fileserver/logs/access.log
maxretry = 5
bantime = 3600
findtime = 600


5. 局域网访问 vs 公网暴露

5.1 仅局域网设置(最安全)


server {
    listen 80;
    server_name files.lan;

# 仅本地子网
allow 192.168.0.0/16;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
deny all;

# ... 其余配置
}

5.2 带多层防护的公网暴露

层级 工具 作用
1 Cloudflare 代理(橙色云) 隐藏源站 IP、DDoS 防护
2 Cloudflare WAF 规则 按国家、ASN 或 UA 拦截
3 nginx allow/deny 二次 IP 过滤
4 Basic 认证 /shared/ 区域的密码网关
5 fail2ban 自动封禁扫描器
6 ufw 防火墙 仅开放 80/443 端口

# ufw — 最小化开放端口
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp          # SSH
sudo ufw allow 80/tcp          # HTTP
sudo ufw allow 443/tcp         # HTTPS
sudo ufw enable

5.3 VPN 替代方案

为获得最高安全性,将文件服务器保持在局域网内,通过 WireGuard 对外暴露:


# /etc/wireguard/wg0.conf(服务器端)
[Interface]
Address = 10.100.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>

[Peer] # 你的笔记本电脑
PublicKey = <peer-public-key>
AllowedIPs = 10.100.0.2/32

[Peer] # 你的手机
PublicKey = <phone-public-key>
AllowedIPs = 10.100.0.3/32

然后通过 http://10.100.0.1 访问文件服务器,无需向互联网暴露任何端口。


6. 维护脚本

6.1 临时文件清理(cron 每小时执行)


#!/bin/bash
# /srv/fileserver/scripts/cleanup.sh
# 删除 /temp/ 目录下超过 60 分钟的文件
find /srv/fileserver/data/temp -type f -mmin +60 -delete


# /etc/cron.d/fileserver
0 * * * * www-data /srv/fileserver/scripts/cleanup.sh

6.2 每日使用报告(cron 每日执行)


#!/bin/bash
# /srv/fileserver/scripts/stats.sh
LOG=/srv/fileserver/logs/access.log
YESTERDAY=$(date -d yesterday +%d/%b/%Y)

echo "=== File Server Daily Report: $(date -d yesterday +%Y-%m-%d) ==="
echo "Total requests: $(grep "$YESTERDAY" "$LOG" | wc -l)"
echo "Unique IPs: $(grep "$YESTERDAY" "$LOG" | awk '{print $1}' | sort -u | wc -l)"
echo "Data served: $(grep "$YESTERDAY" "$LOG" | awk '{sum+=$10} END {printf "%.1f MB\n", sum/1048576}')"


0 8 * * * root /srv/fileserver/scripts/stats.sh | mail -s "FileServer Daily" admin@localhost


7. 故障排查

症状 可能原因 修复方法
413 Request Entity Too Large client_max_body_size 太小 在 nginx 配置中增大
上传卡住 client_body_timeout 太短 设为 600s
静态文件返回 403 Forbidden 文件权限不包含 www-data chown www-data:www-data
/upload 返回 502 Bad Gateway 上传后端未运行 systemctl start fileserver-upload
目录列表为空 autoindex off 或缺少索引文件 设置 autoindex on
nginx CPU 占用高 未启用 sendfile 添加 sendfile on; tcp_nopush on;