运维平台安全设计实战:从 AES 加密到 SSH 指纹校验的 5 道防线
本文属于「码动四季·开源同行」秋季征稿 —— 技术经验体系化沉淀赛道。
以开源项目 AtomOps 为例,分享运维平台建设中 5 层安全防线的工程实现。
为什么运维平台的安全特别难做
运维平台天然持有大量敏感凭据:服务器 root 密码、数据库连接串、SSH 私钥。一旦平台被攻破,攻击者获得的是所有被管服务器的 root 权限——这是典型的"权限放大"场景。
普通 Web 应用被攻破:泄露用户数据。
运维平台被攻破:泄露所有服务器的控制权。
所以 AtomOps 在设计之初就把安全作为第一优先级,构建了 5 道防线:
用户请求 → ① 认证Cookie → ② CSRF校验 → ③ 权限检查 → ④ 凭据加密 → ⑤ SSH指纹校验 → 执行防线一:httpOnly Cookie + 双 Token 机制
问题
传统方案把 JWT 存在localStorage,前端每次请求带上Authorization: Bearer xxx。问题在于:
localStorage可被 JavaScript 读取 →XSS 攻击可窃取 token- token 有效期内无法主动失效
解决方案
双 Token 机制:access_token(短期)+ refresh_token(长期)
# 登录时设置 httpOnly cookieresponse.set_cookie(key="access_token",value=access_token,httponly=True,# JS 不可读,防 XSSsamesite="lax",# 防 CSRFsecure=False,# HTTP 部署时 False,HTTPS 时 Truemax_age=3600,# 1 小过期path="/",)关键点:
httponly=True:JavaScriptdocument.cookie读不到,XSS 偷不走samesite=lax:跨站请求不自动携带 cookie,防 CSRF- 前端用
withCredentials: true让 axios 自动携带 cookie
踩坑:Secure cookie + HTTP = 闪退
生产环境ENV=production导致secure=True,但 HTTP 部署下浏览器静默丢弃Secure cookie。用户登录后所有 API 返回 401,页面闪退回登录页。
教训:secure标志只适用于 HTTPS。HTTP 部署必须设为False,且没有任何浏览器错误提示,极难排查。
防线二:CSRF 双重提交防护
问题
httpOnly cookie 防了 XSS 窃取,但 cookie 会被自动发送到同源请求。攻击者构造一个恶意表单,用户点击后浏览器自动带上 auth cookie 发起 POST 请求——这就是 CSRF。
解决方案
Double Submit Cookie 模式:
1. GET /api/coc/auth/csrf-token → 后端下发 csrf_token cookie 2. 前端读 cookie,放入 X-CSRF-Token 请求头 3. POST 请求 → 后端校验 header token == cookie tokenclassCSRFMiddleware(BaseHTTPMiddleware):SAFE_METHODS={"GET","HEAD","OPTIONS"}asyncdefdispatch(self,request,call_next):ifrequest.methodinself.SAFE_METHODS:returnawaitcall_next(request)# 安全方法放行ifnotrequest.headers.get("Authorization")and\notrequest.cookies.get("access_token"):returnawaitcall_next(request)# 未认证请求交给 401header_token=request.headers.get("X-CSRF-Token","")cookie_token=request.cookies.get("csrf_token","")ifnotheader_tokenornotcookie_token:returnJSONResponse(status_code=403,content={"message":"缺少 CSRF Token"})ifnotsecrets.compare_digest(header_token,cookie_token):returnJSONResponse(status_code=403,content={"message":"CSRF Token 校验失败"})returnawaitcall_next(request)前端配合:axios 拦截器自动在写操作前获取 CSRF token:
asyncfunctionensureCsrfToken(){if(csrfToken)returncsrfTokenconstres=awaitaxios.get('/api/coc/auth/csrf-token')csrfToken=res.data?.csrf_token||readCsrfCookie()returncsrfToken}// 请求拦截器:POST/PUT/DELETE 自动附加 X-CSRF-Tokenif(['post','put','delete'].includes(method)){if(!csrfToken)awaitensureCsrfToken()config.headers['X-CSRF-Token']=csrfToken}为什么不用 SameSite cookie 替代?SameSite=Strict会阻止所有跨站请求,包括从邮件链接打开的合法请求。SameSite=Lax允许 GET 但阻止 POST,需要配合 CSRF token 才能完整防护。
防线三:RBAC 权限控制
实现
# 依赖注入:要求管理员权限asyncdefrequire_admin(user:User=Depends(get_current_user)):ifuser.role!="admin":raiseHTTPException(status_code=403,detail="需要管理员权限")returnuser# 路由使用@router.post("/scripts/{id}/execute")asyncdefexecute_script(user:User=Depends(require_admin)):...前端路由守卫:
constadminOnlyPaths=['/coc/security/key','/audit']if(adminOnlyPaths.some(p=>to.path.startsWith(p))&&role!=='admin'){feedback.error('需要管理员权限')next(from.path||'/')return}防线四:AES 加密存储凭据
问题
主机密码、SSH 私钥如果明文存数据库,DBA 或拖库攻击者直接拿到所有服务器凭据。
解决方案
Fernet 对称加密(AES-128-CBC + HMAC):
fromcryptography.fernetimportFernetdef_derive_key()->bytes:ifsettings.ENCRYPTION_KEY:raw=settings.ENCRYPTION_KEY.encode()returnbase64.urlsafe_b64encode(raw[:32])# 从 SECRET_KEY 派生derived=hashlib.pbkdf2_hmac("sha256",settings.SECRET_KEY.encode(),b"atomops-salt",iterations=100_000,dklen=32)returnbase64.urlsafe_b64encode(derived)_CIPHER=Fernet(_derive_key())defencrypt(text:str)->str:return_CIPHER.encrypt(text.encode()).decode()defdecrypt(cipher_text:str)->str:return_CIPHER.decrypt(cipher_text.encode()).decode()使用方式:
# 创建主机时加密密码host=Host(hostname="web-01",password=encrypt("1qaz@WSX"))db.add(host)# 执行时解密password=decrypt(host.password)ssh.connect(hostname=host.ip,password=password)敏感字段自动加密:后端build_crud_router支持sensitive_fields参数,写入时自动加密,读取时自动脱敏(返回***):
router.include_router(build_crud_router("/api/v1/hosts",Host,"host",sensitive_fields=["password","private_key"],))防线五:SSH 指纹白名单校验
问题
paramiko.AutoAddPolicy()会自动接受任何未知主机密钥——第一次连接时不校验,容易受中间人攻击。RejectPolicy()则拒绝所有未知主机,部署时需要预加载 known_hosts,不够灵活。
解决方案
白名单 + 后置指纹校验:
def_get_client(host:dict)->paramiko.SSHClient:# 1. 白名单检查:主机 IP 必须预先注册指纹known={h["ip"]:h["fingerprint"]forhinlist_known_hosts()}ifhost["ip"]notinknown:raiseSSHException(f"主机{host['ip']}未在 known_hosts 中注册")# 2. AutoAddPolicy 建立连接(不依赖系统 ~/.ssh/known_hosts)client=paramiko.SSHClient()client.set_missing_host_key_policy(paramiko.AutoAddPolicy())client.connect(hostname=host["ip"],password=decrypt(host.password))# 3. 后置校验:实际指纹 vs 记录指纹fingerprint=_get_fingerprint(client.get_transport())ifknown[host["ip"]]!=fingerprint:client.close()raiseSSHException(f"指纹不匹配!期望{known[host['ip']]},实际{fingerprint}")returnclient三层防护逻辑:
- 白名单:未注册的主机直接拒绝,防止连到未知服务器
- AutoAddPolicy:不依赖系统 known_hosts,避免部署环境差异
- 指纹校验:连接后验证服务器身份,防止 DNS 劫持/中间人攻击
部署时自动注册:
# 通过公网 IP 获取指纹,用内网 IP 注册(后端通过内网 SSH)forprivate_ip,public_ipinservers:ssh=paramiko.SSHClient()ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())ssh.connect(public_ip,username='root',password=password)fp=_get_fingerprint(ssh.get_transport())add_known_host(private_ip,fp)五道防线的协作关系
XSS 攻击 │ ┌────────▼─────────┐ │ ① httpOnly Cookie │ ← JS 读不到 token └────────┬─────────┘ │ CSRF 攻击 │ ┌────────▼─────────┐ │ ② CSRF 双重提交 │ ← 跨站请求无 token └────────┬─────────┘ │ 越权访问 │ ┌────────▼─────────┐ │ ③ RBAC 权限控制 │ ← 非管理员被拦截 └────────┬─────────┘ │ 数据库拖库 │ ┌────────▼─────────┐ │ ④ AES 凭据加密 │ ← 密文不可直接使用 └────────┬─────────┘ │ 中间人攻击 │ ┌────────▼─────────┐ │ ⑤ SSH 指纹校验 │ ← 指纹不匹配则断开 └──────────────────┘每道防线针对不同攻击向量,缺一不可。例如:
- 没有 ①,XSS 可窃取 token 绕过 ②③
- 没有 ④,拖库后攻击者直接拿到所有密码
- 没有 ⑤,DNS 劫持可劫持 SSH 连接
总结
| 防线 | 防御目标 | 实现方式 | 踩坑经验 |
|---|---|---|---|
| httpOnly Cookie | XSS 窃取 token | httponly=True | Secure+HTTP 会闪退 |
| CSRF 双重提交 | CSRF 跨站请求 | header+cookie 校验 | 前端需自动获取 token |
| RBAC | 越权访问 | 依赖注入检查角色 | 前端路由守卫配合 |
| AES 加密 | 数据库拖库 | Fernet 对称加密 | 密钥派生要加 salt |
| SSH 指纹 | 中间人攻击 | 白名单+后置校验 | 新主机需自动注册 |
安全没有银弹,每多一道防线,攻击者的成本就高一个数量级。这 5 道防线让 AtomOps 在被攻破单点时,仍能保护最核心的资产——被管服务器的控制权。
项目仓库:https://gitcode.com/cpyaxjq/AtomOps
本文为 AtomGit「码动四季·开源同行」秋季征稿投稿。如果觉得有帮助,欢迎到仓库点个 Star ⭐