Applies to: ✓ Mode 1 · ✓ Mode 2 · ✓ Mode 3
Turn up logging with -v (repeatable). The gateway's --audit-log and the
client's stderr carry the decision detail.
Action mode or capability check fails
The current Action defaults to transparent mode. On macOS, Windows or Linux
without the required Ubuntu TUN/systemd-resolved setup, select mode: proxy.
Use a Node 24-compatible runner and coordinated Action/client releases:
proxy-ready-v1 for proxies, plus proxy-tun-v1 for transparent mode.
client-bin still runs capability
checks; it does not bypass them.
A database client ignores the proxy
psql does not honour ALL_PROXY. Use transparent routes/private DNS in CI or
skimasque exec --forward 15432:db.internal:5432 -- psql -h 127.0.0.1 -p 15432
locally. A shared Cloud gateway cannot reach a private VPC without connectivity;
deploy a customer gateway inside that network.
Transparent routing or private DNS fails
List all private destination CIDRs, including IPv6 for AAAA records. Specify DNS
server IPs and private DNS routing domains; avoid default routes and ~.. Remove
inherited proxy variables. Native flows present IP destinations: policy must
allow IP/CIDR rules and TCP/UDP DNS ports, and the gateway address floor must
permit those ranges. Hostname-only policy does not authorize TUN flows.
ICMP/ping does not work; use probe-target for a real TCP readiness check.
If the client dies, owned private routes become unreachable until cleanup.
Use the Action's state-file and stop.cjs for recovery after an interrupted hook.
An agent still has access after ending its session
Check gateway connectivity to the control plane. Revocations apply when received; an offline gateway uses its cached list until it reconnects, with credential expiry bounding the session. A direct request outside the gateway is not governed by session policy. Use a sandbox to prevent bypass. Runtime labels are audit context and policy max_duration does not enforce expiry; use session TTL.
OIDC exchange fails
Symptom: the client exits at "exchange", or the gateway logs a 401/403
at the token-exchange endpoint.
audmismatch — the token was minted for a different audience. The workflow'spermissions: id-token: writeplus the action'saudience:(or--oidc-audience) must equal one of the gateway's--oidc-audiencevalues, exactly, including scheme.- Wrong issuer — self-managed GitLab / GitHub Enterprise Server needs
--oidc-issuer https://<host>on the gateway. permissions: id-token: writemissing — the runner can't mint a token; the action fails with "no OIDC token available".- Clock skew —
exp/nbfare checked with 60s leeway; a badly skewed runner clock fails verification.
Policy denied the tunnel
Symptom: the tunnel is refused; Proxy-Status carries the reason; a SOCKS
client sees REPLY_NOT_ALLOWED; the job step fails.
- Run
skimasque why <host:port> --app <name> --repository <r> --branch <b>(add--control-planefor the org's published policy). It prints the same decision the gateway logged, plus the closest rules and a suggested rule. [match]didn't match — a fork's PR branch, a different workflow, or the wrongrepository. Check the audit line's identity fields.--appdiffers — the rule'sapplicationmust equal what the client declared.- Transport — a rule with
transport = "udp"won't allow a TCP tunnel (and vice versa).
Gateway unreachable
- QUIC is UDP. Every firewall, security group, and load balancer between CI and the gateway must forward the UDP port (443 or 4433). A TCP-only LB will not work.
--acmealso needs inbound TCP 443 for the Let's Encrypt challenge — publishing the port with-pis not enough if a cloud firewall blocks it.- Check
http://<metrics-listen-address>/readyzon the configured private metrics listener. The QUIC gateway listener is not an HTTPS health endpoint.
Destination unreachable, but policy allowed it
- The SSRF floor. An internal target (
10.x,192.168.x,172.16-31.x,169.254.x, loopback) is refused on the resolved address unless you named its range with--allow-cidr 10.0.5.0/24.--allow-privateopens all of it. - Your firewall. The gateway needs an outbound path to the destination. SkiMasque does not bypass your network controls — allowlist the gateway's egress IP on the target's side (Modes 2–3).
Certificate / ACME failure
--acmepending forever —--hostnamemust resolve to the box and TCP 443 must be reachable from the internet. Watchdocker logsforACME TLS-ALPN-01 challenge listener upthenACME certificate issued.- Rate-limited — Let's Encrypt production has tight limits. Use
--acme-stagingwhile testing, and keep--acme-cacheon a persistent volume so a restart doesn't re-order. account cache store: Permission denied— the image runs as uid 10001; a fresh named volume is root-owned. Bind-mount a host dir youchown 10001:10001first, or userun-gateway.sh.- Wrong environment served —
run-gateway.sh/run-control.shclear the ACME cache when the staging↔production marker changes; a manualdocker rundoes not.
Tunnel expired mid-job
The credential TTL (--credential-ttl, default 1h) is re-exchanged a quarter of
the way before expiry by skimasque-client. If a job still loses the tunnel,
the client process was killed or the relay was restarted — keep the
skimasque-client … socks5 & process alive for the whole job.
Control plane unavailable
This does not stop enforcement. The gateway keeps serving its cached policy:
- past
--control-plane-policy-lease(15m) it reportsdegradedin heartbeats; - past
--control-plane-cache-ttl(30m) it logs an error and sets theskimasque_control_plane_policy_expiredgauge; - it never fails open and never halts the data plane.
Once the control plane returns, the next poll resyncs. If token exchange starts
returning 502, the mint endpoint is down and
--control-plane-no-credential-fallback is set — remove it to allow local
signing during an outage.
Self-hosted gateway registration failed
403 bad_registration_token— the token is spent (single-use) or expired (~1h). Mint a fresh one and, if the gateway half-registered, clear--control-plane-state(rm -rf ~/skimasque/state/*) before retrying.the control plane has no policy for this gateway's organisation yet— publish a policy revision before the first--control-planeboot. The container restart-loops until one exists.relative URL without a base—--control-planeneeds a URL; a bare host getshttps://automatically, but an empty value or a typo like://xdoes not.
skimasque login inside a container fails
Don't. skimasque login is a workstation command — the gateway container runs
one server process and has no writable home. Enrol the gateway with a
registration token instead (--control-plane-token / SKM_CONTROL_TOKEN).