HH-445: deploy production observability and runbooks (#96)

* HH-445: deploy production observability and runbooks

* fix(ops): share production database DSN

* fix(HH-445): enforce database TLS gate

* fix(HH-445): preserve production serve command

* fix(prod): require external database dependencies

* fix(prod): unify database host rejection gates

* test(prod): enforce exact database TLS runbook contract

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-22 19:39:57 +08:00
committed by GitHub
co-authored by rogee
parent 61376a57fd
commit fb83285617
28 changed files with 1622 additions and 220 deletions
+33 -2
View File
@@ -1,9 +1,11 @@
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -292,15 +294,18 @@ func TestValidate_ReleaseDatabaseTLS(t *testing.T) {
}{
{"external disable", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=disable", true},
{"external missing sslmode", "postgres://gochat:database-secret@db.example.test:5432/gochat", true},
{"external duplicate downgrade", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=disable&sslmode=verify-full", true},
{"external duplicate allowed", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=verify-full&sslmode=verify-full", true},
{"external non-fixed certificate path", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=verify-full&sslrootcert=/tmp/ca.crt&sslcert=/run/secrets/external-db-client.crt&sslkey=/run/secrets/external-db-client.key", true},
{"external require", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=require", true},
{"external verify ca", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=verify-ca", false},
{"external verify full", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=verify-full", false},
{"built-in compose disable", "postgres://gochat:database-secret@postgres:5432/gochat?sslmode=disable", true},
{"external fixed certificate paths", "postgres://gochat:database-secret@db.example.test:5432/gochat?sslmode=verify-full&sslrootcert=/run/secrets/external-db-ca.crt&sslcert=/run/secrets/external-db-client.crt&sslkey=/run/secrets/external-db-client.key", false},
} {
t.Run(tt.name, func(t *testing.T) {
cfg.Database.DSN = tt.dsn
if tt.wantErr {
assert.ErrorContains(t, Validate(cfg), "production database DSN must use sslmode")
assert.Error(t, Validate(cfg))
} else {
assert.NoError(t, Validate(cfg))
}
@@ -308,6 +313,32 @@ func TestValidate_ReleaseDatabaseTLS(t *testing.T) {
}
}
func TestValidateProductionDatabaseDSN_RejectsHostMatrix(t *testing.T) {
file, err := os.Open("../../../deploy/docker/database_host_rejection_cases.txt")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, file.Close()) })
scanner := bufio.NewScanner(file)
for scanner.Scan() {
name, dsn, ok := strings.Cut(scanner.Text(), "|")
require.True(t, ok)
t.Run(name, func(t *testing.T) {
assert.ErrorContains(t, ValidateProductionDatabaseDSN(dsn), "must use an external PostgreSQL host")
})
}
require.NoError(t, scanner.Err())
}
func TestProductionDatabaseTLSRunbookContract(t *testing.T) {
runbook, err := os.ReadFile("../../../docs/ops/02-production-operations.md")
require.NoError(t, err)
runbookText := string(runbook)
assert.Equal(t, 1, strings.Count(runbookText, "`sslmode=verify-ca|verify-full`"))
assert.Equal(t, 1, strings.Count(runbookText, "sslmode="))
assert.NotContains(t, runbookText, "sslmode=disable")
assert.NotContains(t, runbookText, "sslmode=require")
}
func TestLoadWithEnv_ProductionRequiresOverlay(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "configs"), 0o755))
+49 -3
View File
@@ -125,9 +125,8 @@ func Validate(cfg *Config) error {
if password, ok := dbURL.User.Password(); !ok || password == "" || containsPlaceholder(password) {
return fmt.Errorf("production database password is required and must not contain placeholders")
}
sslMode := dbURL.Query().Get("sslmode")
if sslMode != "verify-full" && sslMode != "verify-ca" {
return fmt.Errorf("production database DSN must use sslmode=verify-full or verify-ca")
if err := ValidateProductionDatabaseDSN(cfg.Database.DSN); err != nil {
return err
}
if redisURL.User == nil {
return fmt.Errorf("production Redis credentials are required")
@@ -183,6 +182,53 @@ func Validate(cfg *Config) error {
return nil
}
// ValidateProductionDatabaseDSN protects every direct Go database client from
// local targets, duplicate sslmode downgrades, and unsafe certificate paths.
func ValidateProductionDatabaseDSN(dsn string) error {
dbURL, err := url.Parse(dsn)
if err != nil {
return fmt.Errorf("invalid production database DSN: %w", err)
}
hostname := strings.TrimSuffix(strings.ToLower(dbURL.Hostname()), ".")
if zone := strings.LastIndexByte(hostname, '%'); zone >= 0 {
hostname = hostname[:zone]
}
ip := net.ParseIP(hostname)
if hostname == "" || hostname == "postgres" || hostname == "localhost" || ip != nil && ip.IsLoopback() {
return fmt.Errorf("production database DSN must use an external PostgreSQL host")
}
query, err := url.ParseQuery(dbURL.RawQuery)
if err != nil {
return fmt.Errorf("invalid production database DSN query: %w", err)
}
modes := query["sslmode"]
if len(modes) != 1 {
return fmt.Errorf("production database DSN must contain exactly one sslmode")
}
if modes[0] != "verify-ca" && modes[0] != "verify-full" {
return fmt.Errorf("production database DSN must use sslmode=verify-full or verify-ca")
}
fixedPaths := map[string]string{
"sslrootcert": "/run/secrets/external-db-ca.crt",
"sslcert": "/run/secrets/external-db-client.crt",
"sslkey": "/run/secrets/external-db-client.key",
}
usesCertificateFiles := false
for parameter := range fixedPaths {
usesCertificateFiles = usesCertificateFiles || len(query[parameter]) > 0
}
if usesCertificateFiles {
for parameter, path := range fixedPaths {
values := query[parameter]
if len(values) != 1 || values[0] != path {
return fmt.Errorf("production database DSN %s must appear exactly once and use %s", parameter, path)
}
}
}
return nil
}
func containsPlaceholder(value string) bool {
value = strings.ToLower(value)
return strings.Contains(value, "change_me") || strings.Contains(value, "change-me") || strings.Contains(value, "changeme")