Skip to content

Commit 59a2089

Browse files
committed
Harden Go edge services and centralize shared runtime policy
1 parent 2063915 commit 59a2089

File tree

42 files changed

+1208
-435
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1208
-435
lines changed

ARCHITECTURE.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CleanApp Backend Architecture
22

3-
> Last updated: February 16, 2026
3+
> Last updated: March 6, 2026
44
55
## Overview
66

@@ -228,7 +228,7 @@ graph TB
228228
AREAS[Areas Service<br/>:9086]
229229
EMAIL[Email Service<br/>:9089]
230230
TAGS[Tags Service<br/>:9098]
231-
OWN[Ownership Service<br/>:9090]
231+
OWN[Ownership Service<br/>:9096 (prod)]
232232
end
233233
234234
MOBILE --> CS
@@ -242,9 +242,20 @@ graph TB
242242
RL --> RAP
243243
RAP --> TAGS
244244
RAP --> EMAIL
245-
RP --> TAGS
245+
RP --> TAGS
246246
```
247247

248+
### Shared Go Platform Layer (`go-common/`)
249+
250+
The backend now has a small shared Go module for repeated cross-cutting behavior:
251+
252+
- `go-common/appenv`: environment detection, required-secret loading, migration defaults
253+
- `go-common/edge`: centralized CORS, WebSocket origin checks, request-size limits, in-memory token-bucket rate limiting, security headers
254+
- `go-common/serverx`: hardened `http.Server` bootstrap with consistent timeouts
255+
- `go-common/jwtx`: shared JWT parsing helpers for local token validation
256+
257+
This keeps security policy and service bootstrap logic consistent across the Go edge services instead of copying slight variants into each service.
258+
248259
### Public Ingest CLI (`@cleanapp/cli`)
249260

250261
CleanApp ships a public npm CLI package for fetchers/agents:
@@ -479,12 +490,12 @@ graph LR
479490
| Service | Port | Language | Purpose |
480491
|---------|------|----------|---------|
481492
| `cleanapp_report_listener` | 9081 | Go | Receives reports via REST API |
482-
| `cleanapp_report_listener_v4` | 9099 | Go | Updated listener with bulk ingest |
493+
| `cleanapp_report_listener_v4` | 9097 | Rust | Read-optimized v4 API / OpenAPI surface |
483494
| `cleanapp_report_analyze_pipeline` | 9082 | Go | AI analysis (Gemini/OpenAI) |
484495
| `cleanapp_report_processor` | 9087 | Go | Additional processing logic |
485496
| `cleanapp_report_renderer_service` | 9093 | Rust | Image generation |
486497
| `cleanapp_report_tags_service` | 9098 | Rust | Tag management |
487-
| `cleanapp_report_ownership_service` | 9090 | Go | Report assignment |
498+
| `cleanapp_report_ownership_service` | 9096 (prod), 9090 (dev) | Go | Report assignment |
488499

489500
### Social Media & Web Indexing
490501

@@ -761,6 +772,20 @@ Repo tooling:
761772
- Installer/uninstaller: `platform_blueprint/ops/watchdog/`
762773
- Laptop-run golden path: `platform_blueprint/tests/golden_path/golden_path_prod_vm.sh`
763774

775+
### Runtime Migrations
776+
777+
Runtime schema mutation is no longer the default for the hardened Go services.
778+
779+
- Control flag: `DB_RUN_MIGRATIONS`
780+
- Default in production: `false`
781+
- Default in local dev / CI-style environments: `true`
782+
783+
This means:
784+
785+
1. production services fail fast on missing required secrets instead of inventing insecure defaults
786+
2. schema changes should happen through explicit migration/deploy steps
787+
3. service boot is more deterministic and requires fewer database privileges
788+
764789
### Deployment Process
765790
```bash
766791
# Build & tag image
@@ -798,7 +823,7 @@ This flow:
798823
1. **Single database** - All services share one MySQL instance
799824
2. **No auto-scaling** - Manual VM management
800825
3. **No service mesh** - Direct container networking
801-
4. **Limited observability** - Basic Docker logs only (plus VM-local watchdog/smoke checks)
826+
4. **Partial observability** - Public uptime snapshot + VM-local watchdog exist, but centralized logs/traces are still limited
802827
5. **Container conflicts** - Docker Compose naming collisions on redeploy
803828

804829
---

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ where &lt;env&gt; is `LOCAL`, `DEV` or `PROD`.
247247
| report_processor | 9087 | 8080 | Report processing |
248248
| report_renderer_service | 9093 | 8080 | Image rendering |
249249
| report_tags_service | 9098 | 8080 | Tag management |
250-
| report_ownership_service | 9090 | 8080 | Ownership tracking |
250+
| report_ownership_service | 9096 (prod), 9090 (dev) | 8080 | Ownership tracking |
251251

252252
### Authentication & Customer Services
253253
| Service | Host Port | Container Port |

auth-service/config/config.go

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package config
22

33
import (
4-
"crypto/rand"
5-
"encoding/hex"
6-
"log"
7-
"os"
84
"strings"
5+
6+
"cleanapp-common/appenv"
97
)
108

119
type Config struct {
@@ -20,8 +18,12 @@ type Config struct {
2018
JWTSecret string
2119

2220
// Server
23-
Port string
24-
TrustedProxies []string
21+
Port string
22+
TrustedProxies []string
23+
AllowedOrigins []string
24+
RunDBMigrations bool
25+
RateLimitRPS float64
26+
RateLimitBurst int
2527

2628
// OAuth Configuration
2729
GoogleClientID string
@@ -42,58 +44,69 @@ type Config struct {
4244
FrontendURL string
4345
}
4446

45-
func Load() *Config {
46-
cfg := &Config{
47-
DBUser: getEnv("DB_USER", "root"),
48-
DBPassword: getEnv("DB_PASSWORD", "password"),
49-
DBHost: getEnv("DB_HOST", "localhost"),
50-
DBPort: getEnv("DB_PORT", "3306"),
51-
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-here"),
52-
Port: getEnv("PORT", "8080"),
53-
GoogleClientID: getEnv("GOOGLE_CLIENT_ID", ""),
54-
GoogleClientSecret: getEnv("GOOGLE_CLIENT_SECRET", ""),
55-
FacebookAppID: getEnv("FACEBOOK_APP_ID", ""),
56-
FacebookAppSecret: getEnv("FACEBOOK_APP_SECRET", ""),
57-
AppleClientID: getEnv("APPLE_CLIENT_ID", ""),
58-
AppleTeamID: getEnv("APPLE_TEAM_ID", ""),
59-
AppleKeyID: getEnv("APPLE_KEY_ID", ""),
60-
ApplePrivateKey: getEnv("APPLE_PRIVATE_KEY", ""),
47+
func Load() (*Config, error) {
48+
dbPassword, err := appenv.Secret("DB_PASSWORD", "password")
49+
if err != nil {
50+
return nil, err
6151
}
62-
63-
// Handle encryption key
64-
encryptionKey := os.Getenv("ENCRYPTION_KEY")
65-
if encryptionKey == "" {
66-
// Generate a random key for demo - in production, use a fixed key
67-
key := make([]byte, 32)
68-
rand.Read(key)
69-
encryptionKey = hex.EncodeToString(key)
70-
log.Printf("WARNING: Generated temporary encryption key. Set ENCRYPTION_KEY environment variable for production.")
52+
jwtSecret, err := appenv.Secret("JWT_SECRET", "dev-jwt-secret")
53+
if err != nil {
54+
return nil, err
55+
}
56+
encryptionKey, err := appenv.Secret("ENCRYPTION_KEY", devEncryptionKey())
57+
if err != nil {
58+
return nil, err
59+
}
60+
cfg := &Config{
61+
DBUser: appenv.String("DB_USER", "root"),
62+
DBPassword: dbPassword,
63+
DBHost: appenv.String("DB_HOST", "localhost"),
64+
DBPort: appenv.String("DB_PORT", "3306"),
65+
JWTSecret: jwtSecret,
66+
EncryptionKey: encryptionKey,
67+
Port: appenv.String("PORT", "8080"),
68+
GoogleClientID: appenv.String("GOOGLE_CLIENT_ID", ""),
69+
GoogleClientSecret: appenv.String("GOOGLE_CLIENT_SECRET", ""),
70+
FacebookAppID: appenv.String("FACEBOOK_APP_ID", ""),
71+
FacebookAppSecret: appenv.String("FACEBOOK_APP_SECRET", ""),
72+
AppleClientID: appenv.String("APPLE_CLIENT_ID", ""),
73+
AppleTeamID: appenv.String("APPLE_TEAM_ID", ""),
74+
AppleKeyID: appenv.String("APPLE_KEY_ID", ""),
75+
ApplePrivateKey: appenv.String("APPLE_PRIVATE_KEY", ""),
76+
AllowedOrigins: authAllowedOrigins(),
77+
RunDBMigrations: appenv.Bool("DB_RUN_MIGRATIONS", appenv.DefaultRunMigrations()),
78+
RateLimitRPS: float64(appenv.Int("RATE_LIMIT_RPS", 10)),
79+
RateLimitBurst: appenv.Int("RATE_LIMIT_BURST", 20),
7180
}
72-
cfg.EncryptionKey = encryptionKey
7381

7482
// Handle trusted proxies
75-
trustedProxies := os.Getenv("TRUSTED_PROXIES")
76-
if trustedProxies != "" {
77-
cfg.TrustedProxies = strings.Split(trustedProxies, ",")
78-
for i, proxy := range cfg.TrustedProxies {
79-
cfg.TrustedProxies[i] = strings.TrimSpace(proxy)
80-
}
83+
if trustedProxies := appenv.Strings("TRUSTED_PROXIES"); len(trustedProxies) > 0 {
84+
cfg.TrustedProxies = trustedProxies
8185
}
8286

8387
// Email configuration (SendGrid)
84-
cfg.SendGridAPIKey = getEnv("SENDGRID_API_KEY", "")
85-
cfg.SendGridFromName = getEnv("SENDGRID_FROM_NAME", "CleanApp")
86-
cfg.SendGridFromEmail = getEnv("SENDGRID_FROM_EMAIL", "info@cleanapp.io")
88+
cfg.SendGridAPIKey = appenv.String("SENDGRID_API_KEY", "")
89+
cfg.SendGridFromName = appenv.String("SENDGRID_FROM_NAME", "CleanApp")
90+
cfg.SendGridFromEmail = appenv.String("SENDGRID_FROM_EMAIL", "info@cleanapp.io")
8791

8892
// Frontend URL for password reset links
89-
cfg.FrontendURL = getEnv("FRONTEND_URL", "https://cleanapp.io")
93+
cfg.FrontendURL = appenv.String("FRONTEND_URL", "https://cleanapp.io")
9094

91-
return cfg
95+
return cfg, nil
9296
}
9397

94-
func getEnv(key, defaultValue string) string {
95-
if value := os.Getenv(key); value != "" {
96-
return value
98+
func authAllowedOrigins() []string {
99+
if origins := appenv.Strings("ALLOWED_ORIGINS"); len(origins) > 0 {
100+
return origins
101+
}
102+
frontendURL := appenv.String("FRONTEND_URL", "https://cleanapp.io")
103+
origins := []string{frontendURL}
104+
if strings.Contains(frontendURL, "://cleanapp.io") {
105+
origins = append(origins, strings.Replace(frontendURL, "://cleanapp.io", "://www.cleanapp.io", 1))
97106
}
98-
return defaultValue
107+
return origins
108+
}
109+
110+
func devEncryptionKey() string {
111+
return strings.Repeat("0", 64)
99112
}

auth-service/go.mod

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
module auth-service
22

3-
go 1.23.0
3+
go 1.24.0
44

5-
toolchain go1.23.10
5+
toolchain go1.24.1
66

77
require (
8+
cleanapp-common v0.0.0
89
github.com/gin-gonic/gin v1.10.1
910
github.com/go-sql-driver/mysql v1.7.1
1011
github.com/golang-jwt/jwt/v5 v5.2.0
11-
github.com/sendgrid/rest v2.6.9+incompatible
1212
github.com/sendgrid/sendgrid-go v3.14.0+incompatible
1313
golang.org/x/crypto v0.36.0
1414
)
1515

16+
replace cleanapp-common => ../go-common
17+
1618
require (
1719
github.com/bytedance/sonic v1.11.6 // indirect
1820
github.com/bytedance/sonic/loader v0.1.1 // indirect
@@ -31,12 +33,14 @@ require (
3133
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3234
github.com/modern-go/reflect2 v1.0.2 // indirect
3335
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
36+
github.com/sendgrid/rest v2.6.9+incompatible // indirect
3437
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
3538
github.com/ugorji/go/codec v1.2.12 // indirect
3639
golang.org/x/arch v0.8.0 // indirect
3740
golang.org/x/net v0.25.0 // indirect
3841
golang.org/x/sys v0.31.0 // indirect
3942
golang.org/x/text v0.23.0 // indirect
43+
golang.org/x/time v0.12.0 // indirect
4044
google.golang.org/protobuf v1.34.1 // indirect
4145
gopkg.in/yaml.v3 v3.0.1 // indirect
4246
)

auth-service/go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
3434
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
3535
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
3636
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
37-
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
38-
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
39-
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
40-
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
4137
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
4238
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
4339
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -55,6 +51,10 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
5551
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
5652
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5753
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
54+
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
55+
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
56+
github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
57+
github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
5858
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
5959
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
6060
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -84,6 +84,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
8484
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
8585
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
8686
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
87+
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
88+
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
8789
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
8890
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
8991
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=

auth-service/main.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"cleanapp-common/serverx"
45
"database/sql"
56
"fmt"
67
"log"
@@ -22,7 +23,10 @@ import (
2223

2324
func main() {
2425
// Load configuration
25-
cfg := config.Load()
26+
cfg, err := config.Load()
27+
if err != nil {
28+
log.Fatalf("Failed to load config: %v", err)
29+
}
2630

2731
// Database connection
2832
db, err := setupDatabase(cfg)
@@ -31,10 +35,13 @@ func main() {
3135
}
3236
defer db.Close()
3337

34-
// Initialize database schema
35-
log.Println("Initializing database schema and running migrations...")
36-
if err := database.InitializeSchema(db); err != nil {
37-
log.Fatalf("Failed to initialize database schema: %v", err)
38+
if cfg.RunDBMigrations {
39+
log.Println("Initializing database schema and running migrations...")
40+
if err := database.InitializeSchema(db); err != nil {
41+
log.Fatalf("Failed to initialize database schema: %v", err)
42+
}
43+
} else {
44+
log.Println("Skipping runtime database migrations (DB_RUN_MIGRATIONS=false)")
3845
}
3946

4047
// Initialize encryptor
@@ -51,13 +58,13 @@ func main() {
5158

5259
// Start server
5360
log.Printf("Auth service starting on port %s", cfg.Port)
54-
if err := router.Run(":" + cfg.Port); err != nil {
61+
if err := serverx.New(":"+cfg.Port, router).ListenAndServe(); err != nil {
5562
log.Fatalf("Failed to start server: %v", err)
5663
}
5764
}
5865

5966
func setupDatabase(cfg *config.Config) (*sql.DB, error) {
60-
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/cleanapp?parseTime=true&multiStatements=true",
67+
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/cleanapp?parseTime=true",
6168
cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort)
6269

6370
db, err := sql.Open("mysql", dsn)
@@ -128,9 +135,9 @@ func setupRouter(service *database.AuthService, cfg *config.Config) *gin.Engine
128135
router.SetTrustedProxies(cfg.TrustedProxies)
129136

130137
// Apply global middleware
131-
router.Use(middleware.CORSMiddleware())
138+
router.Use(middleware.CORSMiddleware(cfg.AllowedOrigins))
132139
router.Use(middleware.SecurityHeaders())
133-
router.Use(middleware.RateLimitMiddleware())
140+
router.Use(middleware.RateLimitMiddleware(cfg.RateLimitRPS, cfg.RateLimitBurst))
134141

135142
// Initialize email sender (if SendGrid is configured)
136143
var emailSender *email.Sender

0 commit comments

Comments
 (0)