Skip to content

gladly-team/tab-proxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tab-proxy

Tiny reverse proxy that sits in front of the Gladly v5 CloudFront distribution and strips non-whitelisted cookies before forwarding.

Why this exists

CloudFront enforces a 32 KB request-header limit and returns 494 Request Header Too Large when exceeded. Users whose browsers have accumulated a large cookie jar were hitting this. This proxy solves it by dropping every cookie that isn't on a known whitelist before the request reaches the upstream, and by telling the browser to delete the biggest offenders so they stop coming back.

How it works

  1. Client sends request to https://<your-host>/...
  2. Proxy parses the Cookie header and classifies each cookie:
    • Whitelisted → forwarded unchanged upstream
    • Non-whitelisted, pair > 1000 bytes → stripped from the request AND a Set-Cookie: <name>=; Max-Age=0 is emitted in the response to delete it from the browser
    • Non-whitelisted, pair ≤ 1000 bytes → stripped from the request, left alone in the browser
  3. Response Set-Cookie headers from the upstream are also filtered to the whitelist so it can't reintroduce bloat.

Cookie whitelist

Defined in main.go (allowedCookies):

Cookie Purpose
gladly_session Laravel session ID (Redis key)
XSRF-TOKEN CSRF token
TabAuthToken Firebase ID token, set by v5 Blade JS
TabAuth.AuthUser Firebase user object set by tab-web's next-firebase-auth
TabAuth.AuthUser.sig Koa signed-cookie signature for TabAuth.AuthUser
TabAuth.AuthUserTokens Firebase idToken+refreshToken from tab-web's next-firebase-auth
TabAuth.AuthUserTokens.sig Koa signed-cookie signature for TabAuth.AuthUserTokens
tabV4OptIn tab-web v4 opt-in flag
tabV4OptIn.sig Koa signed-cookie signature for tabV4OptIn

Edit the map and redeploy to change it. Threshold constant is deleteCookieThresholdBytes = 1000.

Configuration

All via environment variables. Both modes work with the same binary.

Var Default Notes
UPSTREAM_HOST (unset; required) Hostname requests are forwarded to (e.g. example.cloudfront.net)
TLS_HOST (unset) Set → production mode (autocert on :80 + :443). Unset → dev mode (plain HTTP).
CERT_DIR /var/lib/tab-proxy/certs Where autocert caches Let's Encrypt certs
ACME_EMAIL (unset) Optional; LE uses it for expiry notifications
PORT 8080 Dev mode only (TLS_HOST unset)

Local development

go run .
# listens on :8080 plain HTTP
curl -v http://localhost:8080/health
curl -v -H 'Cookie: gladly_session=x; junk=y' http://localhost:8080/

Build + vet:

go build ./...
go vet ./...

Deployment

./deploy.sh

What it does:

  1. Cross-compiles GOOS=linux GOARCH=arm64 with -trimpath -ldflags='-s -w'
  2. Generates a systemd unit pinned to the current TLS_HOST / ACME_EMAIL
  3. scps binary + unit to the box
  4. Creates system user tabproxy, app dir, cert cache dir (idempotent)
  5. Atomically swaps binary via install(1)
  6. daemon-reload, restart, tails the last 15 journal lines

Overridable env:

SSH_HOST=<ssh-config-alias>         # ~/.ssh/config alias for the target host
TLS_HOST=<your-host>                # hostname (or comma-separated list) for LE cert
ACME_EMAIL=<you@example.com>        # LE notifications

Downtime per deploy: one systemctl restart (~1s) plus at most one RestartSec window (2s).

Infrastructure

Designed for a single-instance deployment. The reference setup is an AWS EC2 t4g.small running Ubuntu 24.04 LTS arm64, with an Elastic IP, a Route 53 A record, and a security group exposing 22/80/443. Any small Linux host with public 80/443 will work — autocert handles cert issuance from there.

App-level restarts are handled by systemd (Restart=always, RestartSec=2s). Pair with platform-level instance recovery (e.g. EC2 automatic recovery, CloudWatch status-check alarms) for hardware failure coverage.

SSH

Recommended: define a host alias in ~/.ssh/config so deploy scripts can reference it by name:

ssh <host-alias>

Direct form:

ssh -i your-key.pem -o IdentitiesOnly=yes ubuntu@<your-host>

IdentitiesOnly=yes is important — without it, ssh-agent offers every loaded key first and the server hits MaxAuthTries before trying the .pem.

Logs

App logs go to systemd-journald on the instance.

Storage Persistent (/var/log/journal/<machine-id>/system.journal)
Size cap 4 GB (SystemMaxUse=4G in /etc/systemd/journald.conf.d/tab-proxy-retention.conf)
Retention 5 days max (MaxRetentionSec=5day); whichever cap hits first wins. At current ~25 req/s burn rate the size cap kicks in around day 4.

Useful commands:

# Live tail
ssh <host-alias> 'sudo journalctl -u tab-proxy -f'

# Last N lines
ssh <host-alias> 'sudo journalctl -u tab-proxy -n 200 --no-pager'

# Time range
ssh <host-alias> 'sudo journalctl -u tab-proxy --since "1 hour ago"'

# Grep cookie events
ssh <host-alias> 'sudo journalctl -u tab-proxy --since today' | grep "cookies deleted"

# Structured JSON
ssh <host-alias> 'sudo journalctl -u tab-proxy -o json'

# Export a chunk to disk
ssh <host-alias> 'sudo journalctl -u tab-proxy --since yesterday --no-pager' > tab-proxy.log

Log format

One line per proxied request. The optional deleted= field is appended only when at least one cookie crossed the size threshold:

<client-ip> (<country>) <method> <path> cookies=<total Cookie-header size> [deleted=<comma-separated names>]

Example:

2026/04/24 21:30:15 192.0.2.10 (US) GET /v5/cookies-test cookies=1.4 KB
2026/04/24 21:30:15 192.0.2.10 (US) POST /v5/login cookies=2.3 KB deleted=bigcookie

Single-line on purpose: two log.Printf calls per request would interleave under concurrent load and break per-IP attribution when grepping the journal.

The client IP is taken from the TCP peer (r.RemoteAddr). X-Forwarded-For is not honored — this proxy is hit directly by clients, so the peer IP is the real client. If a CDN ever ends up in front of this box, add a trusted-proxy allowlist before trusting XFF.

Only cookie names are logged, never values — so session tokens don't leak into logs.

/health hits are not routed through the proxy handler and do not produce these lines.

Runbook

Confirm a request went through this proxy

Every proxied response carries Via: 1.1 tab-proxy (appended to the existing Via chain from CloudFront, per RFC 9110 §7.6.3). One-liner check:

curl -sI https://<your-host>/v5/cookies-test | grep -i via
# via: 1.1 <id>.cloudfront.net (CloudFront)
# via: 1.1 tab-proxy

/health does not include this header — it's served locally and never touches an upstream.

Service status

ssh <host-alias> 'sudo systemctl status tab-proxy'
ssh <host-alias> 'sudo systemctl is-active tab-proxy'

Restart the service

ssh <host-alias> 'sudo systemctl restart tab-proxy'

Rollback a bad deploy

deploy.sh doesn't version artifacts. Easiest rollback is git checkout the previous SHA locally and re-run ./deploy.sh. For faster recovery we'd need to start keeping timestamped binaries under /opt/tab-proxy/releases/ and swapping a symlink — happy to add when needed.

Force a cert renewal

autocert auto-renews ~30 days before expiry. To force early, wipe the cache and restart:

ssh <host-alias> 'sudo rm -rf /var/lib/tab-proxy/certs/* && sudo systemctl restart tab-proxy'
curl -v https://<your-host>/health    # first request re-issues

Change the cookie whitelist or size threshold

Edit allowedCookies or deleteCookieThresholdBytes in main.go, then ./deploy.sh.

systemd hardening

The unit runs the app as the non-root tabproxy user with:

  • CAP_NET_BIND_SERVICE only (to bind :80 and :443)
  • NoNewPrivileges=true
  • ProtectSystem=strict + ReadWritePaths=/var/lib/tab-proxy/certs (cert cache is the only writable path)
  • ProtectHome=true, PrivateTmp=true

Not set up (known gaps)

  • CloudWatch Logs forwarding. Logs are only on the instance. If it gets re-imaged, logs are lost. Ready to wire up the CloudWatch Agent whenever desired.
  • External uptime check. /health exists, but nothing external is hitting it on a schedule. A Route 53 health check ($0.50/mo) + SNS email alarm is the obvious next step.
  • Alarm notifications. The CloudWatch alarms trigger EC2 recovery actions but don't notify anyone. Wire SNS → email/Slack when desired.
  • Versioned releases / rollback. See runbook note above.
  • SSH restricted to a CIDR. Port 22 is currently open to 0.0.0.0/0. Tighten once a stable office/VPN CIDR is available.

About

Tiny reverse proxy that strips non-whitelisted cookies before forwarding to an upstream (originally a CloudFront distribution) to work around CloudFront's 32 KB request-header limit.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors