完善生产健康检查与前端安全配置
This commit is contained in:
parent
ce1a232208
commit
5a592cf250
@ -1,6 +1,14 @@
|
|||||||
http://packages.example.com {
|
http://packages.example.com {
|
||||||
encode gzip
|
encode gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||||
|
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self'; frame-ancestors 'none'"
|
||||||
|
}
|
||||||
|
|
||||||
handle /api/v1/updates/stream {
|
handle /api/v1/updates/stream {
|
||||||
reverse_proxy scout:8080 {
|
reverse_proxy scout:8080 {
|
||||||
flush_interval -1
|
flush_interval -1
|
||||||
|
|||||||
@ -10,6 +10,11 @@ server {
|
|||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
client_max_body_size 512m;
|
client_max_body_size 512m;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self'; frame-ancestors 'none'" always;
|
||||||
|
|
||||||
location /api/v1/updates/stream {
|
location /api/v1/updates/stream {
|
||||||
proxy_pass http://scout:8080;
|
proxy_pass http://scout:8080;
|
||||||
|
|||||||
@ -24,6 +24,10 @@ func (s *Server) hostGuard(next http.Handler) http.Handler {
|
|||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.URL.Path == "/healthz" && isLocalRequest(request) {
|
||||||
|
next.ServeHTTP(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(s.AllowedHosts) > 0 && !hostAllowed(request.Host, s.AllowedHosts) {
|
if len(s.AllowedHosts) > 0 && !hostAllowed(request.Host, s.AllowedHosts) {
|
||||||
writeError(writer, http.StatusMisdirectedRequest, "请求域名不在允许列表中")
|
writeError(writer, http.StatusMisdirectedRequest, "请求域名不在允许列表中")
|
||||||
return
|
return
|
||||||
@ -36,6 +40,16 @@ func (s *Server) hostGuard(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isLocalRequest(request *http.Request) bool {
|
||||||
|
value := strings.TrimSpace(request.RemoteAddr)
|
||||||
|
host, _, err := net.SplitHostPort(value)
|
||||||
|
if err != nil {
|
||||||
|
host = value
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(strings.Trim(host, "[]"))
|
||||||
|
return ip != nil && ip.IsLoopback()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) rateLimit(next http.Handler) http.Handler {
|
func (s *Server) rateLimit(next http.Handler) http.Handler {
|
||||||
if s.RateLimitPerMinute <= 0 {
|
if s.RateLimitPerMinute <= 0 {
|
||||||
return next
|
return next
|
||||||
|
|||||||
@ -33,6 +33,20 @@ func TestHostGuardRejectsUnknownHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocalHealthCheckBypassesProxyGuards(t *testing.T) {
|
||||||
|
server := (&Server{
|
||||||
|
AllowedHosts: []string{"packages.example.com"},
|
||||||
|
ForceHTTPS: true,
|
||||||
|
}).Handler()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8080/healthz", nil)
|
||||||
|
request.RemoteAddr = "127.0.0.1:34567"
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
server.ServeHTTP(response, request)
|
||||||
|
if response.Code != http.StatusOK {
|
||||||
|
t.Fatalf("本机健康检查状态错误: %d", response.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRateLimit(t *testing.T) {
|
func TestRateLimit(t *testing.T) {
|
||||||
server := (&Server{RateLimitPerMinute: 1}).Handler()
|
server := (&Server{RateLimitPerMinute: 1}).Handler()
|
||||||
first := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
first := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||||
|
|||||||
4
使用说明.md
4
使用说明.md
@ -287,7 +287,7 @@ chmod 600 /etc/scout/scout.env
|
|||||||
```bash
|
```bash
|
||||||
cp deploy/scout.env.example deploy/scout.env
|
cp deploy/scout.env.example deploy/scout.env
|
||||||
# 编辑 deploy/scout.env,替换所有生产占位符
|
# 编辑 deploy/scout.env,替换所有生产占位符
|
||||||
docker compose -f deploy/docker-compose.yml up -d --build
|
docker compose -f deploy/docker-compose.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Compose 会启动 Scout API 和前端静态站点:
|
Compose 会启动 Scout API 和前端静态站点:
|
||||||
@ -339,6 +339,8 @@ sudo journalctl -u scout.service -f
|
|||||||
|
|
||||||
外部网关终止 HTTPS 后,必须将 `X-Forwarded-Proto: https` 传给 Scout;否则生产配置中的 `SCOUT_FORCE_HTTPS=true` 会拒绝请求。这是协议边界检查,不是 Scout 的证书管理功能。
|
外部网关终止 HTTPS 后,必须将 `X-Forwarded-Proto: https` 传给 Scout;否则生产配置中的 `SCOUT_FORCE_HTTPS=true` 会拒绝请求。这是协议边界检查,不是 Scout 的证书管理功能。
|
||||||
|
|
||||||
|
`deploy/Caddyfile` 仅是 Caddy 反向代理的参考配置,其中的示例主机名需要替换为实际访问地址。它不会申请或配置 SSL 证书,也不会执行 DNS 解析、域名绑定或服务商 API 操作;如果 SSL 和 DNS 已由其他服务处理,请继续让该服务负责 TLS 终止,并把请求转发到 Scout 的入口。
|
||||||
|
|
||||||
### 健康检查、备份和升级
|
### 健康检查、备份和升级
|
||||||
|
|
||||||
健康检查接口:
|
健康检查接口:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user