1. 为什么第一阶段要先盯住Web与服务器本身
我接手过不少项目,有个规律特别明显:很多团队一谈“安全”就急着上各种扫描器、防火墙,结果业务还没上线先把自己搞瘫痪了。而真正扎实的做法,是先把Web应用和承载它的Web服务器这层地基夯实。这一阶段你投入时间做的每一件事,后面都会变成第一道保护屏障——地基不稳,上面堆再多高级方案都是白搭。
我印象最深的是某次给一套内部工单系统做上线前的安全收口。那套系统功能很简单:用户登录、提交工单、管理员后台审核,技术栈是前端加后端API加数据库,服务器上放着Nginx做反向代理。当时很多人觉得“内网系统嘛,没必要搞得太复杂”,但真把服务器暴露面捋一遍,问题比想象中多得多:默认安装包带了多余的模块、版本号直接暴露在响应头里、没有统一的TLS配置、日志写到一半因为权限不足丢数据……这些都不是什么高深漏洞,但每一个都可能在某天变成被人利用的缝隙。
所以“一阶段—WEB与WEB服务器安全”到底要解决什么问题?我的理解是:先把能远程触达这台服务器和Web应用的所有入口梳理清楚,再把每个入口的默认姿势改造成“最小权限、最小暴露、完整审计”的状态。它不追求某一项技术用得多奇特,而是追求让一个没有任何安全背景的人照着做,也能把一台裸奔的Web服务器收敛到可接受的基线水平。
这个阶段适合谁来学?两类人最需要:一类是刚接手公司Web项目部署的运维或后端工程师,另一类是准备系统学习Web安全、但还没找到入手点的入门者。前者需要一顿能直接抄作业的操作清单,后者需要一套能解释“为什么这样做”的思维框架。这篇文章会把两块都覆盖到,所以读的时候别只照着复制命令,更重要的是理解每一步在对抗什么问题。
我提到的“模拟项目X”就是这么来的:一个虚构的内部工单系统,跑在标准Linux服务器上,外部通过域名访问,前面是Nginx。后面所有章节都围绕这个项目展开,你可以把它替换成任何自己手头的Web服务,思路和方法是通用的。
2. 部署基线的第一步:走一遍请求全链路,找出所有可触达的入口
2.1 画一张请求路径图,别让服务器成为黑盒
很多人加固服务器喜欢从改配置文件开始,我建议反过来——先画图。把所有能触达到这台机器的路径列出来:用户在浏览器输入域名,DNS解析到服务器IP,流量到Nginx的80/443端口,Nginx根据location规则把请求转发给后端应用进程,应用进程再连数据库。这张图看起来简单,但画完之后你会发现自己忽略了几个东西:服务器上的SSH端口是否暴露在公网?数据库端口是否监听在0.0.0.0?监控组件或管理后台是不是开了一个没人记得的端口?
第一阶段的收口工作,本质上就是对着这张图逐段收紧。每一步都要自问:这个节点必须对谁开放?能不能只对特定IP开放?能不能内网访问?能不能干脆关掉?我当时拿“模拟项目X”做实验时,第一步做的不是装补丁,而是检查服务器上有多少监听端口。一条ss -tlnp命令就够,结果发现除了应用需要的80/443,还有3030、9000、3306等一堆端口在对外监听,都是以前调试时留下的服务。全部确认一遍后,能关的关,能限定内网访问的限定内网访问,网络暴露面立刻小了一圈。
2.2 把服务和账号分层,别所有进程共用一个身份
接下来是账号权限。很多入门者习惯拿root启动一切服务,图省事。但试想一下:如果Web应用被攻破,攻击者拿到的是应用进程的权限,而这个权限是root,那服务器基本等于拱手让人。正确的姿势是给每个服务单独建一个低权限账号,比如Nginx跑在nginx用户下,后端应用跑在app用户下,两个账号互相没有写对方目录的权限。
具体操作我把关键点列一下:
- 用
useradd -r -s /sbin/nologin webapp这类命令创建系统用户,禁止登录shell - 应用目录归属权给对应账号,其他账号只有读或执行权限
- 数据库连接使用专用账号,不要用root连库,密码走环境变量或配置文件,不进代码仓库
- Web应用如果涉及文件上传或临时文件目录,单独设置一个可写目录,并确保该目录不能执行脚本
这里有个容易忽略的细节:不只是进程账号要分开,SSH登录账号也要单独管理。我见过不少服务器直接用root加密码登录,这是第一阶段最该杜绝的。至少要关闭root密码登录,改用普通用户加密钥方式,日常需要提权时再通过sudo完成。这样即使密钥泄露,攻击者也拿不到直接的root权限,还能通过审计日志追溯是哪把钥匙进的。
2.3 敏感文件与目录的保护,别把配置当公开资源
另一类常见问题是把不该暴露的文件放在了Web根目录下。比如有人把.env文件、备份的压缩包、Git仓库目录直接丢在网站根目录,访问一个路径就能下载。这类问题不用什么高级攻击手段,就是请求一个URL的事。第一阶段我建议对所有Web目录做一次全面排查,把非必要文件全部移出Web根目录,并且用访问控制规则对点开头、_开头的备份文件、.git目录、配置备份等直接返回403。
在“模拟项目X”里,我还故意给Nginx加了一条规则:对/data、/backup、/.git、*.sql这类路径一律拒绝。测试下来,之前能直接访问到的几个遗留文件全部被挡住了。这个动作的成本极低,但对信息泄露类风险的收益非常直接。
3. 服务器本身的加固:从软件源头到运行环境
3.1 安装源与依赖管理:少一个多余组件就少一个攻击面
操作系统的包管理工具很方便,但便利的另一面是容易装上很多用不到的组件。Nginx默认安装会带上一些模块,如果业务用不到,就没有必要保留。我习惯的做法是装系统时选最小化安装,装软件时按需指定模块,装完后定期用包管理工具查看当前安装了哪些包,凡是不认识或者确认不用的,先封存后清理。
依赖管理也类似。很多Web应用用包管理方式安装一堆第三方库,其中一些库可能已经很久不更新、存在已知安全问题。第一阶段不需要做完美,但至少要建立一个清单:当前应用依赖了哪些关键组件、版本是多少、官方是否还有维护。把这份清单放在部署文档里,后续每次版本升级就有了依据。
3.2 版本信息与默认页面的“隐身”处理
服务器的版本号是没必要让外部知道的。Nginx默认会在响应头里返回Server: nginx/1.20.2之类的信息,攻击者拿到这个信息就可以在漏洞库里精确寻找对应版本。解决办法是在配置里隐藏版本号,甚至可以隐藏软件名。操作上很简单,Nginx的server_tokens off;就能把版本号去掉;同时把默认欢迎页替换掉,避免被顺手探测。
后端应用也是一样。很多框架默认会在错误页面上打印框架版本、路径信息,甚至堆栈日志。第一阶段建议全站开启“生产模式”错误页,统一返回一个简单的错误页面,同时把详细错误记录到日志文件而不是返回给浏览器。这个动作我在“模拟项目X”上做过对比测试:开启前后,同样的错误请求返回内容差别巨大,后者几乎不泄露任何内部信息。
3.3 文件权限、内核参数与基础防护插件
服务器底层还有一些容易被忽略的设置,我不打算全部展开,但至少这几项建议检查:
- 关键配置文件权限保持640或600,属主为root,避免普通用户读取
- 登录失败策略要配置,比如连续失败几次后锁定一段时间,这个工具自带就有,不用额外装服务
- 如果服务跑在Linux下,建议开启防火墙,仅放行对外端口(例如80/443)和受控管理端口,把数据库端口、内部服务端口全部限制在内网或本机
- 资源限制方面,可以用系统自带的服务管理器限制进程能打开的文件数和内存占用,防止某个接口异常时拖死整个进程
这些操作单独看都很小,但合在一起,服务器从“默认状态”变成了“经过收口的状态”,攻击者在第一步探测时就会感觉到阻力。
4. 传输层与HTTP协议层的加固:别让流量裸奔
4.1 TLS证书与协议版本:连接的安全底座
流量明文传输的风险不需要多解释。第一阶段要做的,是为站点配上合法的TLS证书,并显式禁用不安全的协议版本。这里我一般推荐直接用自动化证书工具申请免费证书,比如配置好域名后自动申请和续期,省心且不容易过期。
配置TLS时容易踩的几个坑我也记录一下:
- 只启用TLS 1.2和1.3,不要把旧协议放进来
- 密钥交换算法优先使用前向安全算法,具体配置里表现为某些带
ECDHE前缀的套件优先 - 把HTTP请求强制跳转到HTTPS,避免用户第一次访问时走明文
- 证书私钥文件权限只给属主读写,别让Web进程之外的账号能读
我用“模拟项目X”测试时,把TLS版本限制为1.2以上后,用在线检测工具扫描结果直接亮了绿灯。之前部分地区用户抱怨访问慢,排查后发现是因为某个老客户端还在走旧协议握手,加了前置跳转后问题也随之解决。
4.2 安全响应头:几次点击就能加上的防护层
HTTP响应头是经常被新人忽略的位置,但它们能解决很多实际问题。最常见的几个我列在下面:
Content-Security-Policy:指定页面只能加载来自哪些来源的资源,能有效缓解大部分注入脚本问题X-Frame-Options:禁止页面被嵌入到别人的iframe里,能防点击劫持X-Content-Type-Options:禁止浏览器猜测文件类型,降低类型混淆风险Referrer-Policy:控制页面跳转时是否携带来源信息,减少信息泄露HSTS:告诉浏览器这个站点只能走HTTPS,并缓存一段时间
这些头的配置方式取决于你的Web服务器或框架。Nginx下直接用add_header指令就能加,后端应用框架一般也有中间件可以设置。配置完建议用浏览器开发者工具确认每个响应头都出现且值正确。
4.3 请求体大小、超时与连接数的“风控”配置
Web服务器还有一个容易忽略的侧面:它能接受的请求有多大、连接能挂多久、同时连接数上限是多少。如果这些不限制,一个简单的慢速请求就能占住连接资源,服务很容易被拖垮。第一阶段建议至少配置:
- 请求体大小上限:例如
client_max_body_size 10m;,防止超大请求上传拖垮应用 - 超时时间:读超时、写超时、连接超时都设置合理值
- 单IP连接数和速率限制:避免来自单点的突发流量
这些参数不是越大越好,也不是越小越好,需要结合业务体量来定。比如文件上传类应用,请求体上限就要放宽;普通接口服务,限制就收紧。我一般先观察一周的正常访问日志,取一个比峰值高一些的值,从而兼顾可用性和风险控制。
5. 应用层边界:输入、输出与身份验证的“三块闸门”
5.1 输入侧:把外部数据一律视为不可信
Web安全中最经典的一类问题,根源都是对输入没有做足够的约束。这里说的输入不只是表单字段,还包括URL参数、请求头、Cookie、上传文件名,甚至从第三方接口拿回来的数据。第一阶段不需要把每种攻击手法都背下来,但需要建立一个生产习惯:所有外部进来的数据,先验证再使用。
具体到“模拟项目X”上,我做了几个典型改进:
- 查询参数全部改为参数化查询,不再直接拼进SQL字符串
- 数字类型参数做白名单校验,长度和范围有明确上限
- 上传文件按扩展名和内容类型双重校验,并随机重命名存储,原始文件名不进存储目录
- 对外输出的内容按位置分别处理:HTML上下文做转义,URL上下文做编码,JS上下文中特殊字符转安全形式
这些改造做完后,我用自动化扫描工具跑了一遍,之前能探测到的注问题明显下降。更重要的收获是,同事们在代码评审时开始主动追问“这个参数是哪来的”,这个意识比任何工具都值钱。
5.2 输出侧:编码与转义,让用户提交的内容“只是内容”
输出侧的XSS问题也很常见。很多团队知道要过滤输入,但面对富文本内容时,一刀切过滤会把正常功能弄坏。我的思路是:允许用户提交一定范围内的复杂内容,但输出到不同场景时必须做对应的上下文编码。
举个例子,用户在工单系统里填了一段描述,里面带了<script>标签,数据库存的就是原样。如果后端返回给浏览器时直接把它当作HTML渲染,脚本就执行了。但如果在输出前对HTML做转义,<变成<,浏览器只会把它显示成文本。这就够了。对于确实需要展示富文本的场景,可以采用白名单标签过滤方案,只允许保留很有限的几个安全标签。
5.3 身份验证与访问控制:让“你是谁”和“你能做什么”分开
身份管理和越权问题,是很多内部系统的大坑。第一阶段至少要建立两个概念:认证(你是谁)和授权(你能做什么),并把它们区分开。常见的问题是登录后,用户能直接通过修改URL里的ID去查看别人的工单。修复思路是在后端对每个请求都做一次“当前用户是否有权限访问这个资源”的判断,而不是简单查一下数据是否存在就返回。
密码存储方面同样要强制规范:绝对不能明文存密码。至少要用带盐的哈希算法处理用户密码,登录时用相同算法比对。密码长度策略建议不低于8位,最好强制包含多种字符类型。管理员账号建议额外开启两步验证,这一步能挡住绝大多数撞库和弱口令攻击。
6. 日志、监控与异常响应:第一阶段就要养成的“哨兵习惯”
6.1 日志到底要记什么:访问日志、错误日志、审计日志
安全建设里最容易“做样子”的就是日志。很多服务器日志文件一直都在写,但没人看,出了问题也不知道去哪查。第一阶段我建议按三个维度记录:
- 访问日志:谁在什么时间访问了什么路径、返回什么状态码、UA是什么
- 错误日志:应用报错堆栈、数据库连接异常、超时信息
- 审计日志:管理员登录、配置变更、敏感操作记录
“模拟项目X”落地时,我给Nginx和后端应用分别配置了滚动日志,按天切分并保留30天。访问日志格式里专门加了$request_time,排查慢接口时非常有用。这些日志不需要多华丽,但一定要保得住、查得到。
6.2 最简单的异常检测:不看全量,只看“异常倍数”
很多团队一听“安全监控”就觉得要上商业平台,其实第一阶段完全可以从最简单的阈值告警开始。观察一周正常流量后,确定一个基线,比如平均每分钟请求数、错误率、单个IP的访问频率,然后把告警阈值设为基线的5倍或10倍。超过时通过通知工具发一条消息到工作群。这套方案不需要额外基建,一条脚本就能完成。
实际操作中要避免的坑是阈值太灵敏产生告警疲劳。我见过一个项目把告警阈值定得太低,群里每天刷几百条,后来大家直接把群屏蔽了。正确的做法是:先静默观察一段时间,再逐步开启告警,并且每条告警都要给出可操作的上下文,而不是只写“异常”。
6.3 突发情况下的第一反应:先断网、再取证、后恢复
如果我怀疑服务器已经被人拿下了,建议按下面的顺序操作,而不是慌着去看日志:
- 第一步,先把疑似问题服务从网络中断开,例如在云控制台安全组里封禁外部IP或关停公网入口,防止继续被试探
- 第二步,保留现场,把当前进程列表、网络连接、日志文件做快照再备份,不要立即删除任何内容
- 第三步,用备份环境或独立环境做排查,不要在原机器上反复猜测
- 第四步,恢复时先从备份还原干净的代码和数据,再逐项收敛之前的配置问题
这个流程我跟很多同事讲过一个类比:服务器被入侵相当于家里进了贼,第一件事是关门报警,而不是先把屋子翻一遍恢复秩序。关门就是断网,报警就是备份取证,后面才是整理现场。顺序反了,证据丢了,问题也会反复出现。
7. 踩坑记录:几个看起来小、后患却很大的习惯
7.1 只改配置不重启验证,等于没改
第一阶段最容易犯的错是改完配置就认为生效了。比如Nginx配置语法没问题,但配置里写错了一个参数名,服务能起来,行为却和预期完全不符。正确做法是:每次改完配置,用配置检查工具验证语法,平滑重载服务,然后用实际请求测试目标是否生效。验证的方式要具体,比如隐藏版本号后,用curl -I看响应头里是否真的没有版本字段;限制了上传大小后,实际发一个大请求确认返回413。
7.2 证书过期是“定时炸弹”,自动化才能保平安
TLS证书的申请不是难点,续期才是。我接手过的项目中,不止一次出现过证书过期导致用户打不开页面的情况。证书有效期短的证书尤其考验续期流程。第一阶段建议直接用自动化工具配置,在证书到期前自动更新,再配一条到期提醒兜底。确认续期成功后,别忘了重载Web服务,否则新证书文件已经替换,但进程还在用旧证书。
7.3 安全头配置要分场景,不能一把锁全锁死
很多文章建议直接加一串安全头,但实际业务中,某些配置会和现有功能冲突。最常见的是Content-Security-Policy:一个页面如果用了内联脚本,或者引用了第三方CDN资源,而策略里没有放行,页面功能会直接变成一片空白。我的经验是:安全头从“宽松到严格”逐步收紧。先用报告模式观察一段时间,看哪些资源被拦截,再决定是调整业务代码还是给策略加白名单。一步到位很容易出现“安全了,但功能没了”的尴尬局面。
7.4 日志权限不足导致丢数据,比没日志更可怕
日志如果因为权限问题写不进去,服务可能不会崩,但会静默丢失关键信息。排查问题时翻日志却发现缺失,这个情况最让人头疼。我在“模拟项目X”上就遇过一次:应用进程账号没有日志目录的写权限,错误日志只在启动时出现了几行,后面的内容全没了。折腾半天找到原因后,我把日志目录的属主、轮转机制、空间占用上限全部重新配了一遍。这个细节建议在部署清单里专门列出来,每次上线前检查一次。
8. 这一阶段做完之后,自我检查与后续方向
第一阶段做完了,怎么判断自己真的做到位了?我给几个简单的自检方式:
- 用一个外部IP访问服务器,只开放必要端口,其他一律拒绝
- 隐藏服务器版本号,错误页面不泄露堆栈和路径
- 全站强制HTTPS,打开浏览器地址栏看到锁标识
- 访问几个常见的敏感路径,比如
/.git、/.env、备份文件,都返回403或404 - Web应用输入参数全部走参数化查询,输出内容按上下文编码
- 登录和后台操作有完整日志,日志可持续查询
- 有一个应急预案:发现异常后,你能在10分钟内完成断网和取证
做完这些,“一阶段—WEB与WEB服务器安全”这扇门基本就站稳了。我自己的体会是:安全建设没有一劳永逸,但第一阶段的这些基础动作,性价比是最高的。它们不依赖商业产品、不需要团队规模、不要求高深算法,只要有人愿意花时间把每一项落到实际环境和代码里,就能把一大批常见风险挡在门外。
后续如果你想继续深入,方向其实清晰:可以学习怎么分析一次恶意请求的完整链条,可以研究身份认证体系的单点登录与权限模型,可以尝试做更细粒度的数据分类分级。但不管走哪个方向,都请记住——先把地基这一阶段扎实地走完,再用工具和理论去解决更具体的问题。服务器不是上了锁就安全,而是每一层都清楚自己在防什么,才能睡得着觉。