Skip to content

Commit c3a781f

Browse files
retlehsswalkinshawclaude
committed
Replace chi router with net/http ServeMux
Remove the chi dependency in favor of Go 1.22+ ServeMux patterns and a small set of stdlib middleware (Recoverer, RealIP, TimeoutHandler). Drop ADMIN_ALLOW_CIDR and RequireAllowedIP — production already disabled IP restriction. Admin access relies solely on in-app auth. Keep TRUST_PROXY so clientIP() works behind Caddy for rate limiting and telemetry dedupe. Co-Authored-By: Scott Walkinshaw <scott.walkinshaw@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ce6f8cd commit c3a781f

File tree

16 files changed

+322
-300
lines changed

16 files changed

+322
-300
lines changed

deploy/ansible/group_vars/production/main.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ r2_cdn_public_url: "https://cdn.wp-composer.com"
3232
# Sessions
3333
session_lifetime_minutes: 7200
3434

35-
# Admin access (empty = no IP restriction, set Tailscale CIDRs when ready)
36-
admin_allow_cidr: ""
37-
3835
# Repository
3936
repository_path: "{{ app_root }}/shared/storage/repository"
4037

deploy/ansible/roles/app/templates/env.j2

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ LOG_LEVEL={{ go_log_level }}
66
DB_PATH={{ db_path }}
77
LISTEN_ADDR={{ go_listen_addr }}
88
TRUST_PROXY=true
9-
10-
ADMIN_ALLOW_CIDR={{ admin_allow_cidr }}
119
SESSION_LIFETIME_MINUTES={{ session_lifetime_minutes }}
1210

1311
SEEDS_FILE={{ seeds_file }}

docs/admin-access.md

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,17 @@
22

33
## Security Model
44

5-
Admin access uses defense in depth with two independent layers:
5+
Admin access is protected by in-app authentication. Email/password login and admin authorization are required for all protected `/admin/*` routes.
66

7-
1. **Network layer** — Tailscale restricts access to `/admin/*` routes. Only devices on the tailnet can reach admin endpoints.
8-
2. **Application layer** — in-app authentication (email/password) and admin authorization required for all protected `/admin/*` routes.
7+
## Proxy Trust
98

10-
Both layers must pass. A valid Tailscale connection without app auth (or vice versa) is denied.
11-
12-
## Network Restriction
13-
14-
The Go app enforces IP-based access control on all `/admin/*` routes via the `ADMIN_ALLOW_CIDR` config.
15-
16-
Default allowed ranges (Tailscale):
17-
- `100.64.0.0/10` (Tailscale IPv4)
18-
- `fd7a:115c:a1e0::/48` (Tailscale IPv6)
19-
20-
Override via environment:
21-
22-
```env
23-
ADMIN_ALLOW_CIDR=100.64.0.0/10,fd7a:115c:a1e0::/48,10.0.0.0/8
24-
```
25-
26-
Set to empty to disable IP restriction (development only):
27-
28-
```env
29-
ADMIN_ALLOW_CIDR=
30-
```
31-
32-
If all configured CIDRs are invalid, the middleware fails closed — all admin access is blocked until the configuration is corrected.
33-
34-
### Proxy trust
35-
36-
By default, the app uses the raw `RemoteAddr` for IP checks. If behind a reverse proxy (Caddy, nginx), enable proxy header trust:
9+
When behind a reverse proxy (Caddy, nginx), enable proxy header trust so the app sees the real client IP (used for login rate limiting and telemetry dedupe):
3710

3811
```env
3912
TRUST_PROXY=true
4013
```
4114

42-
This enables Chi's `RealIP` middleware, which reads `X-Forwarded-For` / `X-Real-IP` headers to determine the client IP. **Only enable this when the app is behind a trusted proxy** — otherwise clients can spoof their IP via forwarding headers and bypass admin IP restrictions.
15+
This reads `X-Real-IP` / `X-Forwarded-For` headers to determine the client IP. **Only enable this when the app is behind a trusted proxy** — otherwise clients can spoof their IP.
4316

4417
## Admin Bootstrap
4518

@@ -78,18 +51,6 @@ wpcomposer cleanup-sessions
7851

7952
Run via systemd timer or cron (daily recommended).
8053

81-
## Exposure Verification
82-
83-
To verify admin is not publicly accessible:
84-
85-
```bash
86-
# From outside the tailnet — should return 403
87-
curl -s -o /dev/null -w "%{http_code}" https://app.example.com/admin/login
88-
89-
# From inside the tailnet — should return 200
90-
curl -s -o /dev/null -w "%{http_code}" https://app.example.com/admin/login
91-
```
92-
9354
## Emergency Password Reset
9455

9556
If locked out of the admin panel:

docs/architecture.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ WP Composer has two primary runtime concerns:
2929
### Web UI
3030

3131
- Public package browser/detail pages via server-rendered Go templates + Tailwind.
32-
- Admin panel at `/admin` with Tailscale network gating + in-app auth (defense in depth).
32+
- Admin panel at `/admin` with in-app auth (email/password + session).
3333

3434
## Module Layout
3535

@@ -43,7 +43,7 @@ internal/
4343
├── repository/ artifact generation, hashing, integrity validation
4444
├── deploy/ local promote/rollback/cleanup + R2 sync
4545
├── telemetry/ event ingestion, dedupe, rollups
46-
└── http/ Chi router, handlers, templates, static assets
46+
└── http/ stdlib router, handlers, templates, static assets
4747
```
4848

4949
## Data Flow
@@ -93,7 +93,7 @@ storage/repository/
9393
- `GET /admin/packages` — package management
9494
- `GET /admin/builds` — build history/status
9595
- Admin-triggered sync/build/deploy actions
96-
- Access: Tailscale network gating + in-app auth/authorization required
96+
- Access: in-app auth/authorization required
9797

9898
## Static Repository Deployment
9999

docs/development.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ Env-first with optional YAML config file (env overrides YAML).
6868
| `R2_SECRET_ACCESS_KEY` || R2 credentials |
6969
| `R2_BUCKET` || R2 bucket name |
7070
| `R2_ENDPOINT` || R2 S3-compatible endpoint |
71-
| `ADMIN_ALLOW_CIDR` | Tailscale ranges | Comma-separated CIDRs for `/admin/*` access |
7271
| `TRUST_PROXY` | `false` | Trust `X-Forwarded-For` for client IP (enable behind proxy) |
7372
| `SESSION_LIFETIME_MINUTES` | `7200` | Admin session lifetime |
7473

@@ -77,14 +76,14 @@ Env-first with optional YAML config file (env overrides YAML).
7776
| Area | Choice |
7877
|------|--------|
7978
| CLI | Cobra |
80-
| HTTP router | Chi |
79+
| HTTP router | `net/http` (stdlib) |
8180
| Migrations | Goose (SQL-first) |
8281
| Templates | `html/template` + Tailwind |
8382
| Logging | `log/slog` |
8483
| SQLite driver | `modernc.org/sqlite` |
8584
| R2 | AWS SDK for Go v2 |
8685
| Config | env-first + optional YAML |
87-
| Admin access | Tailscale network gating + in-app auth |
86+
| Admin access | In-app auth (email/password + session) |
8887

8988
## Make Targets
9089

docs/operations.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,7 @@ echo 'new-password' | wpcomposer admin reset-password --email admin@example.com
165165

166166
## Admin Access Model
167167

168-
Admin access uses defense in depth:
169-
170-
1. **Network layer** — Tailscale restricts access to `/admin/*` routes to authorized devices on the tailnet.
171-
2. **Application layer** — in-app authentication and admin authorization required for all `/admin/*` routes.
172-
173-
Both layers must pass. A valid Tailscale connection without app auth (or vice versa) is denied.
168+
Admin access is protected by in-app authentication (email/password) and admin authorization for all `/admin/*` routes.
174169

175170
## Server Provisioning
176171

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ require (
88
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1
99
github.com/fogleman/gg v1.3.0
1010
github.com/getsentry/sentry-go v0.43.0
11-
github.com/go-chi/chi/v5 v5.2.5
1211
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
1312
github.com/pressly/goose/v3 v3.27.0
1413
github.com/spf13/cobra v1.10.2

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
3232
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
3333
github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4=
3434
github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
35-
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
36-
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
3735
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
3836
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
3937
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=

internal/config/config.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ type DBConfig struct {
3030
}
3131

3232
type ServerConfig struct {
33-
Addr string `yaml:"addr"`
34-
AdminAllowCIDR []string `yaml:"admin_allow_cidr"`
35-
TrustProxy bool `yaml:"trust_proxy"`
33+
Addr string `yaml:"addr"`
34+
TrustProxy bool `yaml:"trust_proxy"`
3635
}
3736

3837
type R2Config struct {
@@ -68,8 +67,7 @@ func defaults() *Config {
6867
LogLevel: "info",
6968
DB: DBConfig{Path: "./storage/wpcomposer.db"},
7069
Server: ServerConfig{
71-
Addr: ":8080",
72-
AdminAllowCIDR: []string{"100.64.0.0/10", "fd7a:115c:a1e0::/48"}, // Tailscale default ranges
70+
Addr: ":8080",
7371
},
7472
Session: SessionConfig{LifetimeMinutes: 7200},
7573
Telemetry: TelemetryConfig{
@@ -159,13 +157,6 @@ func applyEnv(cfg *Config) {
159157
if v := os.Getenv("TRUST_PROXY"); v != "" {
160158
cfg.Server.TrustProxy = strings.EqualFold(v, "true") || v == "1"
161159
}
162-
if v, ok := os.LookupEnv("ADMIN_ALLOW_CIDR"); ok {
163-
if v == "" {
164-
cfg.Server.AdminAllowCIDR = nil // explicitly disable restriction
165-
} else {
166-
cfg.Server.AdminAllowCIDR = strings.Split(v, ",")
167-
}
168-
}
169160
if v := os.Getenv("SEEDS_FILE"); v != "" {
170161
cfg.Discovery.SeedsFile = v
171162
}

internal/http/handlers.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"time"
2121

2222
"github.com/getsentry/sentry-go"
23-
"github.com/go-chi/chi/v5"
2423
"github.com/roots/wp-composer/internal/app"
2524
"github.com/roots/wp-composer/internal/config"
2625
"github.com/roots/wp-composer/internal/deploy"
@@ -197,8 +196,8 @@ func handleRootsWordpress(a *app.App, tmpl *templateSet) http.HandlerFunc {
197196

198197
func handleDetail(a *app.App, tmpl *templateSet) http.HandlerFunc {
199198
return func(w http.ResponseWriter, r *http.Request) {
200-
pkgType := chi.URLParam(r, "type")
201-
name := chi.URLParam(r, "name")
199+
pkgType := r.PathValue("type")
200+
name := r.PathValue("name")
202201

203202
// Strip wp- prefix from type
204203
pkgType = strings.TrimPrefix(pkgType, "wp-")
@@ -630,7 +629,7 @@ func ogImageURL(cfg *config.Config, key string) string {
630629
// handleOGImage serves OG images from local disk (dev mode).
631630
func handleOGImage() http.HandlerFunc {
632631
return func(w http.ResponseWriter, r *http.Request) {
633-
key := chi.URLParam(r, "*")
632+
key := strings.TrimPrefix(r.URL.Path, "/og/")
634633
clean := filepath.Clean(key)
635634
if strings.Contains(clean, "..") {
636635
http.NotFound(w, r)

0 commit comments

Comments
 (0)