V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要把任何和邀请码有关的内容发到 NAS 节点。

邀请码相关的内容请使用 /go/in 节点。

如果没有发送到 /go/in,那么会被移动到 /go/pointless 同时账号会被降权。如果持续触发这样的移动,会导致账号被禁用。
happyxhw101
V2EX  ›  NAS

分享下我的 nas 安全方案

  •  
  •   happyxhw101 · 2 天前 · 1543 次点击
    1. 物理机直接安装 ubuntu, 所有应用都部署在 docker
    2. ssh 只允许密钥登录, 禁止 root 用户登录
    3. 所有访问( http, tcp)都通过 nginx 代理, ufw 只暴露固定的几个端口, nginx 开启 https 证书
    4. nginx 配置 geolite2, 禁止任何 国外 ip 访问, 异常访问基本都是国外 ip
    5. fail2ban 自动封禁所有 nginx 日志里面国外 ip
    6. 不安装 1panel,宝塔等任何 web 管理工具, 直接 ssh 到机器上命令行管理

    分享下我的 nginx 配置

    load_module "modules/ngx_http_geoip2_module.so";
    load_module "modules/ngx_stream_geoip2_module.so";
    
    worker_processes 4;
    
    error_log /var/log/nginx/nginx_error.log;
    error_log /var/log/nginx/nginx_error.log notice;
    error_log /var/log/nginx/nginx_error.log info;
    
    pid /var/log/nginx/nginx.pid;
    
    events {
        worker_connections 1024;
    }
    
    
    http {
        include /etc/nginx/mime.types;
        default_type application/octet-stream;
    
        geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
          auto_reload 24h;
          $geoip_country_code  default=Unknown source=$remote_addr country iso_code;
          $geoip_country_name  country  names  en;
        }
        geoip2 /etc/nginx/geoip/GeoLite2-City.mmdb {
          auto_reload 24h;
          $geoip_city   default=Unknown city names en;
        }
    
        map $geoip_country_code $allowed_country {
            default no;
            CN yes;
        }
    
        map $remote_addr $allowed {
            default $allowed_country;
            127.0.0.1 yes;
            ~^192\.168\.\\d+\.\\d+$ yes;
            ~^172\.16\.0\.\\d+$ yes;
            ~^172\.17\.\\d+\.\\d+$ yes;
        }
    
        map $http_upgrade $connection_upgrade {
            default upgrade;
            '' "";
        }
    
        log_format json_analytics escape=json '{'
        '"timestamp": "$msec", ' # request unixtime in seconds with a milliseconds resolution
        '"request_id": "$request_id", ' # the unique request id
        '"request_length": "$request_length", ' # request length (including headers and body)
        '"body_bytes_sent": "$body_bytes_sent", '
        '"remote_addr": "$remote_addr", ' # client IP
        '"time_iso8601": "$time_iso8601", '
        '"request_uri": "$request_uri", ' # full path and arguments if the request
        '"code": "$status", ' # response status code
        '"http_host": "$http_host", ' # the request Host: header
        '"server_name": "$server_name", ' # the name of the vhost serving the request
        '"request_time": "$request_time", ' # request processing time in seconds with msec resolution
        '"upstream": "$upstream_addr", ' # upstream backend server for proxied requests
        '"request_method": "$request_method", ' # request method
        '"allowed": "$allowed", '
        '"geoip_country_code": "$geoip_country_code", '
        '"geoip_country_name": "$geoip_country_name", '
        '"geoip_city": "$geoip_city"'
        '}';
    
        access_log /var/log/nginx/access.log json_analytics;
        error_log /var/log/nginx/error.log warn;
    
        set_real_ip_from 0.0.0.0/0;
        real_ip_header X-Real-IP;
        real_ip_recursive on;
    
        sendfile on;
        server_tokens off;
        keepalive_timeout 65;
    
        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 application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject 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;
    
        proxy_buffering off;
        proxy_buffers 4 128k;
        proxy_buffer_size 256k;
        proxy_busy_buffers_size 256k;
    
    
    
        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers ALL:!DH:!EXPORT:!RC4:+HIGH:+MEDIUM:-LOW:!aNULL:!eNULL;
    
        ssl_certificate /etc/nginx/ssl/fullchain.cer;
        ssl_certificate_key /etc/nginx/ssl/xxx.cc.key;
    
    
        include /etc/nginx/conf.d/*.conf;
    
    }
    
    
    stream {
    
        geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
          auto_reload 24h;
          $geoip_country_code  default=Unknown source=$remote_addr country iso_code;
          $geoip_country_name  country  names  en;
        }
        geoip2 /etc/nginx/geoip/GeoLite2-City.mmdb {
          auto_reload 24h;
          $geoip_city   default=Unknown city names en;
        }
    
    
    
        map $geoip_country_code $allowed_country {
            default no;
            CN yes;
        }
    
        map $remote_addr $allowed {
            default $allowed_country;
            127.0.0.1 yes;
            ~^192\.168\.\\d+\.\\d+$ yes;
            ~^172\.16\.0\.\\d+$ yes;
            ~^172\.17\.\\d+\.\\d+$ yes;
        }
    
        log_format json_analytics escape=json '{'
        '"timestamp": "$msec", ' # request unixtime in seconds with a milliseconds resolution
        '"connection": "$connection", ' # connection serial number
        '"pid": "$pid", ' # process pid
        '"remote_addr": "$remote_addr", ' # client IP
        '"remote_port": "$remote_port", ' # client port
        '"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
        '"upstream": "$upstream_addr", '
        '"protocol": "$protocol", '
        '"allowed": "$allowed", '
        '"request_method": "STREAM", '
        '"geoip_country_code": "$geoip_country_code", '
        '"geoip_country_name": "$geoip_country_name", '
        '"geoip_city": "$geoip_city"'
        '}';
    
        access_log /var/log/nginx/access.log json_analytics;
        error_log /var/log/nginx/error.log warn;
    
        include /etc/nginx/stream.d/*.conf;
    }
    
    

    ssh 代理

    map $allowed $ssh_server {
        yes ssh;
    }
    
    upstream ssh {
        server  192.168.5.1:1234;
    }
    
    server {
        listen    5678;
        listen [::]:5678;
        proxy_pass $ssh_server;
        proxy_connect_timeout 30s;
        proxy_timeout 60s;
    
        ssl_preread on;
    }
    

    http 代理

    server {
        server_name x.x.com;
        listen 1233 ssl;
        listen [::]:1233 ssl;
    
        http2 on;
        charset "utf-8";
    
        if ($allowed != yes) {
            return 404;
        }
    
        error_page 497 =307 https://$host:$server_port$request_uri;
    
        client_max_body_size 512M;
        proxy_buffering off;
    
    
        set $backend "http://192.168.5.1:1234";
        include /etc/nginx/conf.d/basic/no_log.conf;
    
        location / {
            proxy_redirect off;
            proxy_set_header Host $host:$server_port;
            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_pass $backend;
        }
    
    }
    
    
    19 条回复    2026-02-04 12:57:16 +08:00
    xingheng
        1
    xingheng  
       1 天前   ❤️ 1
    这就叫专业.jpg
    swiftg
        2
    swiftg  
       1 天前 via iPhone
    不对公众提供网络服务,只服务自己和有限的几个人的话,完全不用暴露任何端口到公网
    wonderfulcxm
        3
    wonderfulcxm  
       1 天前 via iPhone
    你这个也防不了飞牛的路径穿越,还是得加个 waf 。
    dilidilid
        4
    dilidilid  
       1 天前
    @wonderfulcxm nginx 在 docker 里,只要不是把根目录挂上去了就算穿越也只能在 docker 里面玩
    abc6666
        5
    abc6666  
       1 天前
    我都是飞牛前面套了一个 WAF 。就可以了。
    wwd179
        6
    wwd179  
       1 天前
    我用 tailscale 组私网。nas 只对公网暴露 tailscale 端口。
    有些要对外的服务用 docker 运行 nginx 这些反代。
    wonderfulcxm
        7
    wonderfulcxm  
       1 天前 via iPhone
    @dilidilid 如果是飞牛,我理解没错的话,要用 fn connect 要 nginx 反代到宿主机的 5666 或者 5667 端口。无论是不是 docker ,加 waf 是过滤,nginx 就纯是转发了
    yinmin
        8
    yinmin  
       1 天前
    nginx 可以换一个思路保护:
    (1) 必须域名访问: stream 模块里加 map $ssl_preread_server_name,不要设置 default 。如果黑客不知道 SNI,nginx 会直接挂断 TCP 连接,不会进行 TLS 协商
    (2) 启用双向证书(mTLS)保护: HTTPS 网站在 nginx 里启用客户端证书认证,如果没有客户端证书 nginx 就无法建立 tls 连接,保护后端业务。windows 、macos 、ios 、android 的浏览器都是原厂支持客户端证书的

    实施以上 2 条后,可以不用禁止国外 ip
    WizardLeo
        9
    WizardLeo  
       1 天前
    只开放 vpn 、p2p 端口,实在要用反代也套一层 nginx 用 sni 进行端口复用。
    moioooo
        10
    moioooo  
       1 天前 via iPhone
    防火墙设置只允许特定 ip 访问(内网除外)就行,然后找个小鸡搭个代理。客户端用这个代理访问 nas 就行。
    只要你的代理不泄漏,别人也访问不了 nas 了。
    PerFectTime
        11
    PerFectTime  
       1 天前
    开 ss 回家,要映射服务到公网的前置套一层 oauth 认证
    benjaminliangcom
        12
    benjaminliangcom  
       1 天前
    @wonderfulcxm #3 其实直接用 CLoudfalre tunnel 就行了,可以零信任,可以 mtls ,也可以 WAF
    zhouu
        13
    zhouu  
       1 天前
    我用 pangolin 这套方案
    realdaniel
        14
    realdaniel  
       1 天前 via Android
    ubuntu 可以安装 knockd ,要用时开门,用完关门。
    archxm
        15
    archxm  
       1 天前 via Android
    首先要有实际需求,有了数据,之后,才考虑安全。
    否则你 80%时间精力,都在思考,这安不安全,如何才能安全!
    likooo125802023
        16
    likooo125802023  
       1 天前
    直接用内网回来,啥安全都不用,反正也不用公开给别人访问
    AutumnVerse
        17
    AutumnVerse  
       1 天前
    我的方案:

    物理机是 pve ,内部有 10 多个虚拟机。所有内网服务禁止公网入站,仅有需要的机器允许公网出站。

    内网机器安装 xray ,每分钟通过 http 给外网机器发送消息。外网机器记录服务器 ip ,然后生成 clash 订阅链接。我手机、公司电脑安装 claash ,添加内网订阅,然后在任何地方都能无感进入家里内网
    Ketteiron
        18
    Ketteiron  
       1 天前
    1. 物理机安装 ubuntu
    2. tailscale
    完事,搞得越复杂反而越容易出错
    xingheng
        19
    xingheng  
       1 天前
    @archxm 直接假定“我要远程访问自己拍的毛片”
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2552 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 11:03 · PVG 19:03 · LAX 03:03 · JFK 06:03
    ♥ Do have faith in what you're doing.