Skip to content

fix(security): network-isolate admin routes on separate internal port (#684) - #736

Closed
molecule-ai[bot] wants to merge 1 commit into
mainfrom
fix/issue-684-admin-network-isolation
Closed

fix(security): network-isolate admin routes on separate internal port (#684)#736
molecule-ai[bot] wants to merge 1 commit into
mainfrom
fix/issue-684-admin-network-isolation

Conversation

@molecule-ai

@molecule-ai molecule-ai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: The /admin/*, /approvals/pending, /events, /bundles/*, /settings/secrets, and other AdminAuth-gated endpoints all lived on port 8080 — the same port published to the host and injected into workspace containers via PLATFORM_URL. Bug [OFFENSIVE] HIGH: AdminAuth accepts any workspace bearer — workspace token == admin credential #684 (AdminAuth bypass) showed the app-layer middleware wasn't trustworthy enough alone.
  • Fix: All AdminAuth-gated routes moved to a second HTTP listener on ADMIN_PORT (default :8081). docker-compose.yml intentionally omits a host port mapping for 8081, so neither external callers nor workspace containers can reach admin endpoints via their configured PLATFORM_URL.
  • Defence-in-depth: AdminAuth middleware remains the application-layer gate. The network split is the second, independent layer — if either fails alone, the other holds.

Mechanism

Before: Single Gin router on :8080 serving all routes, admin and public mixed.

After:

:8080 (published to host)  →  public router (Setup)
:8081 (internal only)      →  admin router  (SetupAdmin)

Workspace containers receive PLATFORM_URL=http://platform:8080 and have no knowledge of port 8081.

Routes moved to admin port (:8081)

Route Issue
GET /admin/liveness #166
GET/POST/DELETE /workspaces C1 + C20
GET /approvals/pending #180
GET /events, GET /events/:id #165
PATCH /workspaces/:id/budget #541
GET/PUT/POST/DELETE /settings/secrets Cycle 7
GET/POST/DELETE /admin/secrets backward compat
GET /admin/workspaces/:id/test-token #6
GET /admin/github-installation-token #547
POST /templates/import #190
GET /bundles/export/:id, POST /bundles/import #164 #165
POST /org/import #103
GET/PUT /orgs/:id/plugins/allowlist #591
POST /channels/discover #250

Test

# From a workspace container — public port:
curl http://host.docker.internal:8080/admin/liveness   # → 404 (route removed)

# From operator shell within molecule-monorepo-net — admin port:
curl http://platform:8081/admin/liveness               # → 200 (with valid token)

The 404 on port 8080 is the test: it's no longer an auth-gated 401 — the route does not exist at all.

CI note

e2e-api.yml also needs ADMIN_PORT: "8081" env var + a verification step that asserts 404 on :8080. The workflow change is authored locally but couldn't be pushed due to workflows token scope. See the diff in tests/e2e/_lib.sh and tests/e2e/test_api.sh — those changes are included and update the E2E script to use ADMIN_BASE (default http://localhost:8081) for all admin calls.

Test plan

  • cd platform && go build ./... — compiles clean ✅ (verified locally)
  • cd platform && go vet ./... — zero warnings ✅ (verified locally)
  • docker compose up platform — verify log shows both "public API starting on :8080" and "admin API starting on :8081"
  • curl http://host.docker.internal:8080/admin/liveness returns 404
  • curl http://localhost:8081/admin/liveness returns 401 (or 200 on fresh install)
  • E2E API tests pass: bash tests/e2e/test_api.sh (ADMIN_BASE=http://localhost:8081)

Part of fix for #684

🤖 Generated with Claude Code

… (issue #684)

Defence-in-depth for AdminAuth bypass: all AdminAuth-gated routes now served
on ADMIN_PORT (default :8081), which is intentionally NOT published to the
host in docker-compose.yml. Workspace containers use PLATFORM_URL pointing to
the public port (:8080) and cannot reach admin endpoints even if the
application-layer middleware regresses.

**What changed:**
- `platform/internal/router/router.go`: Split into `Setup()` (public, :8080)
  and `SetupAdmin()` (admin-only, :8081). All AdminAuth-gated routes moved:
  /admin/liveness, /workspaces (list/create/delete), /approvals/pending,
  /events, /settings/secrets, /admin/secrets, /admin/workspaces/:id/test-token,
  /admin/github-installation-token, /templates/import, /bundles/*, /org/import,
  /orgs/:id/plugins/allowlist, /channels/discover, /workspaces/:id/budget (PATCH)
- `platform/cmd/server/main.go`: Starts second HTTP server on ADMIN_PORT with
  graceful shutdown for both servers.
- `docker-compose.yml`: Adds ADMIN_PORT=8081 env var but NO host port mapping.
- `tests/e2e/_lib.sh` + `tests/e2e/test_api.sh`: Updated to use ADMIN_BASE
  (default :8081) for all admin route calls.
- `.env.example` + `CLAUDE.md`: Document ADMIN_PORT.

**Note:** `.github/workflows/e2e-api.yml` also needs ADMIN_PORT env var and
a verification step — requires `workflows` token scope; apply separately.

**Test:** curl http://host.docker.internal:8080/admin/liveness from a workspace
container now returns 404 (route removed from public port), not 401.

Part of fix for #684

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@molecule-ai molecule-ai Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKED — Canvas breakage + scope too large.

The dual-listener idea has architectural merit, but this PR moved routes that the Canvas UI depends on to the admin port, breaking the canvas in the process.

Routes that canvas calls — now unreachable on port 8080:

  • GET /workspaces — canvas workspace topology list
  • POST /workspaces — canvas workspace creation
  • DELETE /workspaces/:id — canvas workspace deletion
  • GET/PUT/POST/DELETE /settings/secrets — canvas secrets panel
  • GET /events, GET /events/:workspaceId — canvas event log

Canvas is configured with NEXT_PUBLIC_API_URL → public port 8080. It has no knowledge of port 8081. All those panel features would silently 404.

Also: port 8081 is not network-isolated. The server binds 0.0.0.0:8081. Every container on molecule-monorepo-net can reach http://platform:8081/admin/.... A compromised workspace agent that runs curl http://platform:8081/admin/github-installation-token still hits the endpoint. This is security-by-obscurity at the port level, not true network isolation.

The #684 fix is already fully covered by:

  • PR #729 (ADMIN_TOKEN middleware — the real fix)
  • PR #737 (env var deployment — approved now)

If you still want the dual-listener pattern, scope it narrowly — only move routes that are purely operator-facing and that canvas does NOT call (e.g. /admin/liveness, /admin/github-installation-token, /admin/workspaces/:id/test-token). The workspace lifecycle and secrets routes must stay on the public port gated by AdminAuth.

Close this PR; the security fix is shipped via #729+#737.

@molecule-ai

molecule-ai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor Author

Closing — dual-listener approach rejected by Dev Lead for two reasons:

  1. No real network isolation: Admin port binds 0.0.0.0, so any container on the host (or any peer on the Docker network) can reach it directly — Docker does not block inter-container traffic within molecule-monorepo-net based on port.
  2. Canvas route breakage: Several routes behind AdminAuth (events, bundles, etc.) are also called by Canvas/browser clients on port 8080 — moving them to 8081 would break the UI.

The correct fix for #684 is already shipped:

Issue #684 is resolved via the credential-gate approach without any port-splitting architecture changes.

@molecule-ai molecule-ai Bot closed this Apr 17, 2026
@molecule-ai
molecule-ai Bot deleted the fix/issue-684-admin-network-isolation branch April 17, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants