Skip to content

feat(gateway): security-hardening (findings #1–#15, excl. Aikido)#67

Merged
insuT0ver merged 3 commits into
mainfrom
experiment/48-security-hardening
Jul 16, 2026
Merged

feat(gateway): security-hardening (findings #1–#15, excl. Aikido)#67
insuT0ver merged 3 commits into
mainfrom
experiment/48-security-hardening

Conversation

@insuT0ver

@insuT0ver insuT0ver commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Squashed security-hardening work on top of main (operator-auth + source-IP-gate
removal already landed via #47). The finding numbers below refer to the internal
security review, not GitHub issues (none were opened). Internal review/design docs
and the Aikido extension changes (findings 6 and 14) are intentionally left out of
this branch.

  • Socket-proxy HostConfig allowlist (findings 1–2): reject VolumesFrom,
    DeviceCgroupRules, DeviceRequests and the device family (host/daemon
    takeover, raw-disk access).
  • CONNECT hostname casing (finding 3): canonicalize the host + COLLATE NOCASE
    so casing can't bypass an exact deny carved out of a wildcard allow.
  • Path-allowlist normalization (finding 7): fail-closed on ../ and ..%2f,
    segment boundaries; the normalized path is forwarded upstream.
  • Volume ownership (finding 8): named-volume huddle.parent check +
    GET /volumes filtered to the caller's own volumes.
  • Folder-mapping SQL injection (finding 9): column allowlist kills the
    JSON-key injection; route returns 400/404 instead of 500.
  • Extension integrity (finding 11): SHA-256 check of the bundle before
    extraction (allowlist).
  • Token placeholder scoping (finding 12): bound to the requesting container
    (timing-safe) + TTL; no shared unknown bucket; redacted from the audit body.
  • PTY join visibility (finding 13): multi-attach logs attach:owner vs
    attach:join(N) so a join is never silent (regression test added).
  • IDE credential channel (finding 15): VS Code Machine settings (workspace
    trust, no auto-tasks, no local terminal, null credential env) + a container-side
    guard that strips the forwarded git credential.helper from /etc/gitconfig
    and ~/.gitconfig, tears down the forwarded GPG/SSH/askpass sockets, and
    re-applies on inotify events (fallback poll). JetBrains gets the shared
    env/socket scrub. Verified live: git push and git credential fill no longer
    obtain host creds.
  • e2e: authenticate with HUDDLE_OPERATOR_TOKEN; split devcontainer→API into
    proxy path (403) and direct path (401); targeted pre-flight on auth misconfig.

How to try it (experiment 48)

Pushing this branch publishes a full experiment build (CLI + gateway/base images)
under the tag experiment-48, so reviewers can test end-to-end on their own
Docker/Podman host:

# als je al op een ander experiment zit: eerst resetten (anders blijft de CLI
# op de oude versie hangen — die vergelijkt alleen het experimentnummer)
huddle experiment reset

huddle experiment use 48        # installeert CLI + images van experiment 48 en init't
# of, in één keer vanuit een schone stand:
huddle init --experiment 48

Terug naar de stabiele versie:

huddle experiment reset

Snelle rooktest van de finding-15-fix in een verse devcontainer (host-creds mogen
er niet meer in zitten):

printf 'protocol=https\nhost=github.com\n\n' | GIT_TERMINAL_PROMPT=0 git credential fill   # geen password=
git push --dry-run origin HEAD:refs/heads/_test                                             # faalt op auth

Related issue

No GitHub issues were opened for these; the finding numbers track the internal
security review (findings 1–15). Addressed here: 1, 2, 3, 7, 8, 9, 11, 12, 13, 15.
Deliberately not in this branch (remain open): findings 6 and 14 (Aikido
extension, handled separately).

Type of change

  • Bug fix (fix)
  • New feature (feat)
  • Documentation (docs)
  • Refactor / maintenance (refactor / chore)
  • Other:

Checklist

  • My branch is up to date with main and focused on a single change.
  • The project builds and type-checks locally.
  • Tests pass (npm --prefix gateway test) and I added/updated tests where relevant.
  • I ran Prettier and did not reformat unrelated code.
  • Commit messages follow Conventional Commits.
  • I did not introduce logging of secrets, tokens, or credentials.
  • Documentation is updated where needed (README / relevant docs).

Notes for reviewers

  • Bouwt op Support voor podman #47: operator-token-auth en het verwijderen van de source-IP-gate
    zaten al in main via Support voor podman #47; deze branch is de resterende hardening.
  • Finding 15 is containerkant defense-in-depth, niet formeel 100%. Het
    attach-kanaal wordt door de host-IDE gedreven; de guard maakt het gebruik van
    doorgestuurde credentials onmogelijk (helper-strip + socket-teardown via
    inotify). Live geverifieerd in een herstarte devcontainer: git push faalt op
    auth en git credential fill geeft geen token meer. De structureel-airtight
    route (server-side IDE achter de proxy) is Phase 1.
  • Neveneffect van de guard: binnen de devcontainer werkt push met de
    doorgestuurde host-credentials niet meer — dat is het beoogde gedrag. Pushen
    vanuit de container vergt nu een bewust eigen token/deploy-key.
  • Findings 6 en 14 (Aikido) blijven open — bewust uit deze branch gehouden;
    beleg ze bij de Aikido-revisie zodat ze niet tussen wal en schip vallen.
    Finding 6 is een High (langlevend OAuth-secret + globale API-key in de untrusted
    container).
  • Lokale build/test niet gedraaid: npm install voor de gateway wordt door de
    Huddle-firewall geblokkeerd; typecheck/vitest/e2e leunen op CI. Wel gedaan:
    POSIX-syntaxchecks van de gegenereerde config-shell en functionele checks van de
    guard-logica.
  • Testen (na build in CI / lokaal met engine):
    # in een verse devcontainer
    printf 'protocol=https\nhost=github.com\n\n' | GIT_TERMINAL_PROMPT=0 git credential fill   # geen password=
    ls -la ~/.gnupg/S.gpg-agent*                                                                # bestaat niet
    git push --dry-run origin HEAD:refs/heads/_test                                             # faalt op auth
    

Squashed hardening work on top of main (operator-auth + source-IP-gate
removal already landed via #47). Internal review/design docs and the Aikido
extension changes (findings #6/#14) intentionally left out of this branch.

- #1/#2 socket-proxy HostConfig allowlist: reject VolumesFrom, DeviceCgroupRules,
  DeviceRequests and the device family (host/daemon takeover, raw-disk access).
- #3 CONNECT hostname canonicalization + COLLATE NOCASE so casing can't bypass
  an exact deny carved out of a wildcard allow.
- #7 path-allowlist normalization (fail-closed on ../ and ..%2f, segment
  boundaries), normalized path forwarded upstream.
- #8 named-volume ownership check (huddle.parent) + GET /volumes filtered.
- #9 folder-mapping column allowlist kills the JSON-key SQL injection; route
  returns 400/404 instead of 500.
- #11 extension-bundle SHA-256 integrity check before extraction (allowlist).
- #12 token placeholder bound to the requesting container (timing-safe) + TTL;
  no shared 'unknown' bucket; redacted from the audit body.
- #13 PTY multi-attach logs attach:owner vs attach:join(N) so a join is never
  silent (regression test added).
- #15 IDE credential channel: VS Code Machine settings (workspace trust, no
  auto-tasks, no local terminal, null credential env) + a container-side guard
  that strips the forwarded git credential.helper from /etc/gitconfig and
  ~/.gitconfig, tears down the forwarded GPG/SSH/askpass sockets, and re-applies
  on inotify events (fallback poll). JetBrains gets the shared env/socket scrub.
  Verified live: git push and `git credential fill` no longer obtain host creds.
- e2e: authenticate with HUDDLE_OPERATOR_TOKEN; split devcontainer→API into
  proxy path (403) and direct path (401); targeted pre-flight on auth misconfig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@insuT0ver
insuT0ver requested a review from a team July 15, 2026 18:42
Comment thread gateway/src/api.ts Outdated
Comment thread gateway/src/api.ts Outdated
Two Aikido findings on the finding-3 domain handling:
- non-string `domain` (truthy but not a string) passed the truthiness check and
  then threw on .toLowerCase() → 500. Require typeof === 'string' → clean 400.
- storing the domain lowercased mutated the value clients get back ("may break
  clients expecting original casing"). It was redundant anyway: matching is
  already case-insensitive (COLLATE NOCASE in db.ts + canonicalizeHost/matchDomain).
  Store the domain as provided; the requested-row cleanup DELETE gets COLLATE
  NOCASE so it still matches the lowercase requested rows the proxy creates.

Finding #3 stays closed; echo-back to clients is preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@insuT0ver
insuT0ver force-pushed the experiment/48-security-hardening branch from 3e1c6fd to 21b5584 Compare July 15, 2026 19:04
The finding-3 casing test expected a 403 from `curl https://gist.github.com`,
but a host-level HTTPS deny refuses the CONNECT tunnel (rejectSocket → 403 on
the CONNECT response). curl then reports http_code '000' — the 403 lives in
http_connect_code, not http_code (unlike the plain-HTTP deny path, which does
surface 403). So the request came back '000', not '403', and e2e failed.

Assert '000' for both the exact and the capitalized host: a refused CONNECT is
the correct, deterministic signal that the deny holds (no upstream egress
needed). If casing bypassed the deny, the host would match the wildcard allow
and the CONNECT would be accepted — not '000'. Path-mode 403 assertions
(finding 7) are unchanged: those keep the tunnel open and return 403 inside the
MITM'd session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@insuT0ver
insuT0ver merged commit fba08c7 into main Jul 16, 2026
8 checks passed
@insuT0ver
insuT0ver deleted the experiment/48-security-hardening branch July 20, 2026 06:52
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.

1 participant