feat(gateway): security-hardening (findings #1–#15, excl. Aikido)#67
Merged
Conversation
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>
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
force-pushed
the
experiment/48-security-hardening
branch
from
July 15, 2026 19:04
3e1c6fd to
21b5584
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Squashed security-hardening work on top of
main(operator-auth + source-IP-gateremoval 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.
VolumesFrom,DeviceCgroupRules,DeviceRequestsand the device family (host/daemontakeover, raw-disk access).
COLLATE NOCASEso casing can't bypass an exact deny carved out of a wildcard allow.
../and..%2f,segment boundaries; the normalized path is forwarded upstream.
huddle.parentcheck +GET /volumesfiltered to the caller's own volumes.JSON-key injection; route returns 400/404 instead of 500.
extraction (allowlist).
(timing-safe) + TTL; no shared
unknownbucket; redacted from the audit body.attach:ownervsattach:join(N)so a join is never silent (regression test added).trust, no auto-tasks, no local terminal, null credential env) + a container-side
guard that strips the forwarded git
credential.helperfrom/etc/gitconfigand
~/.gitconfig, tears down the forwarded GPG/SSH/askpass sockets, andre-applies on inotify events (fallback poll). JetBrains gets the shared
env/socket scrub. Verified live:
git pushandgit credential fillno longerobtain host creds.
HUDDLE_OPERATOR_TOKEN; split devcontainer→API intoproxy 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 ownDocker/Podman host:
Terug naar de stabiele versie:
Snelle rooktest van de finding-15-fix in een verse devcontainer (host-creds mogen
er niet meer in zitten):
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
fix)feat)docs)refactor/chore)Checklist
mainand focused on a single change.npm --prefix gateway test) and I added/updated tests where relevant.Notes for reviewers
zaten al in
mainvia Support voor podman #47; deze branch is de resterende hardening.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 pushfaalt opauth en
git credential fillgeeft geen token meer. De structureel-airtightroute (server-side IDE achter de proxy) is Phase 1.
doorgestuurde host-credentials niet meer — dat is het beoogde gedrag. Pushen
vanuit de container vergt nu een bewust eigen token/deploy-key.
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).
npm installvoor de gateway wordt door deHuddle-firewall geblokkeerd; typecheck/vitest/e2e leunen op CI. Wel gedaan:
POSIX-syntaxchecks van de gegenereerde config-shell en functionele checks van de
guard-logica.