security: harden remote-access stack (pentest remediation R1-R4)
App layer (R1): bind frps NewProxy to token-owned subdomain (anti-hijack), default-deny unknown webhook ops, HMAC-verify stripe-stub billing webhook, enforce bandwidth quota kill-switch (Ping op), least-privilege table grants (migrations 0002/0003), GOTRUE_PASSWORD_MIN_LENGTH=12. Infra/net (R2): unpublish internal host ports (kong/pooler/analytics/frps-dash), read-only docker-socket-proxy for vector (no host breakout), on-demand-TLS allow-list authorizer, edge-block machine-only webhooks, no-new-privileges on custom containers. Secrets (R3): rotate Postgres password (all roles) + frps dashboard; replace predictable supavisor defaults; secrets externalized to gitignored .env. Med/Low (R4): security response headers (HSTS/XCTO/XFO/Referrer/Permissions/COOP), restrict frp proxy_type to http (no open relay), disable destructive redis commands, tighten frps.toml perms. No secrets committed; rotated values live only in gitignored .env files.
This commit is contained in:
+33
-9
@@ -1,10 +1,27 @@
|
||||
{
|
||||
email office@linumiq.com
|
||||
on_demand_tls {
|
||||
# Self-hosted ask endpoint on :9999 (always 200 in Wave A).
|
||||
# TODO Wave B: point ask at https://api.linumiq.net/functions/v1/check-subdomain
|
||||
# (an Edge Function that returns 200 only for subdomains present in tunnels table).
|
||||
ask http://localhost:9999/check
|
||||
# SECURITY (R2/F3): closed allow-list authorizer. The edge function returns
|
||||
# 200 only for reserved hosts (apex/app/api) and subdomains registered in
|
||||
# the tunnels table; 403 otherwise. This prevents unbounded on-demand
|
||||
# certificate issuance for arbitrary hostnames.
|
||||
ask http://supabase-edge-functions:9000/check-subdomain
|
||||
}
|
||||
}
|
||||
|
||||
# SECURITY (R4/F10/W5): baseline response-hardening headers applied to the
|
||||
# LinumIQ-controlled surfaces (apex/app/api). HSTS forces HTTPS for a year and
|
||||
# is safe for first-party hostnames we fully control.
|
||||
(security_headers) {
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "no-referrer"
|
||||
Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||
Cross-Origin-Opener-Policy "same-origin"
|
||||
-Server
|
||||
-X-Powered-By
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +30,7 @@ linumiq.net {
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
import security_headers
|
||||
redir https://app.linumiq.net{uri} permanent
|
||||
}
|
||||
|
||||
@@ -21,6 +39,7 @@ app.linumiq.net {
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
import security_headers
|
||||
reverse_proxy web:3000
|
||||
}
|
||||
|
||||
@@ -29,18 +48,23 @@ api.linumiq.net {
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
import security_headers
|
||||
# SECURITY (R2): block machine-only webhook functions from the public edge.
|
||||
# auth-webhook and stripe-webhook are invoked internally over the docker
|
||||
# network (supabase-edge-functions:9000) and must never be callable from the
|
||||
# internet. get-node stays public for the Home Assistant add-on.
|
||||
@blocked_webhooks path /functions/v1/auth-webhook* /functions/v1/stripe-webhook*
|
||||
respond @blocked_webhooks 403
|
||||
reverse_proxy supabase-kong:8000
|
||||
}
|
||||
|
||||
# Wildcard tunnel subdomains -> frps vhost HTTP. Per-name HTTP-01 issued on first hit.
|
||||
# NOTE: only HSTS is injected here; Home Assistant sets its own security headers
|
||||
# (X-Frame-Options, etc.) and we must not override its CSP / framing behaviour.
|
||||
*.linumiq.net {
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
header Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
reverse_proxy frps:7080
|
||||
}
|
||||
|
||||
# Internal ask endpoint for on-demand TLS. Bound to loopback inside the container.
|
||||
http://localhost:9999 {
|
||||
respond /check 200
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user