Skip to content

Access Control

Tunnel Whisperer enforces access control at two scopes. Relay admission is per-server, decided at the TLS handshake by a mutual-TLS client certificate. User authorization is per-user, decided by the SSH public key and its port restrictions. The two are independent: a connection must clear the certificate gate to reach the relay at all, and then clear the SSH key check to do anything on the server.


Relay Admission — Mutual TLS (per server)

The relay's Caddy front door is configured with client_auth require_and_verify. Every connection must present an X.509 client certificate signed by the server's own certificate authority, or the TLS handshake is rejected before any tunnel is established.

  • The certificate is per server, not per user — every user of a server shares the same client.crt/client.key, delivered in the config bundle.
  • Admission is decided at the handshake, shielding the relay's downstream machinery from anonymous traffic.
  • This is the primary transport-layer gate.

This layer is covered in full — CA generation, certificate distribution, the Caddy configuration, and the patched Xray-core that presents the certificate — on the Relay Authentication page.


Transport-Layer Identity — Xray UUID (defense-in-depth)

Each user is assigned a unique Xray UUID during the tw create user wizard. This UUID is registered in the relay's Xray configuration file (/usr/local/etc/xray/config.json).

  • Each UUID maps to a single user
  • Removing a UUID from the relay config prevents that user from establishing a tunnel through it
{
  "inbounds": [{
    "settings": {
      "clients": [
        { "id": "server-uuid-here" },
        { "id": "alice-uuid-here" },
        { "id": "bob-uuid-here" }
      ]
    }
  }]
}

No longer the security boundary

Admission is now decided by the mutual-TLS certificate gate above. A caller that cannot present a trusted client certificate never reaches the point where the UUID is checked, so the UUID functions as defense-in-depth rather than the primary control.


SSH Public Key Auth

All SSH authentication uses Ed25519 key pairs. Password authentication is disabled entirely.

  • Each user receives an individual key pair generated by tw create user
  • The server maintains an authorized_keys file with one entry per user
  • The embedded SSH server (Go x/crypto/ssh) validates the client's public key against this file on every connection attempt

No password fallback

The SSH server does not support password authentication under any circumstances. If a client loses their private key, a new key pair must be generated and the old public key removed from authorized_keys.


Per-User Port Restrictions

The permitopen directive in authorized_keys limits each user to forwarding traffic only to specific 127.0.0.1:<port> targets. Users cannot access the server's wider network or forward to arbitrary ports.

permitopen="127.0.0.1:5432",permitopen="127.0.0.1:8080" ssh-ed25519 AAAA... alice@tw

In this example, user alice can forward to:

  • 127.0.0.1:5432 (e.g., PostgreSQL)
  • 127.0.0.1:8080 (e.g., a web application)

Any attempt to forward to a port not listed in permitopen is rejected by the SSH server.

Localhost only

The remote host in port forwarding is locked to 127.0.0.1. Users cannot specify external hosts — all forwarded traffic targets services running on the server machine itself.


Dynamic Authorization

The embedded SSH server re-reads authorized_keys on every authentication attempt. This means:

  • Adding a new user takes effect immediately — no restart of tw serve required
  • Revoking a user's key takes effect on the next connection attempt
  • There is no cached state that could allow a revoked key to authenticate

This design ensures that access control changes are applied in real time, without service interruption.


User Revocation

To fully revoke a user's access, two actions are required:

Step 1 — Remove the SSH public key

Remove the user's entry from the server's authorized_keys file. This blocks SSH authentication on the next connection attempt.

Step 2 — Remove the Xray UUID from the relay

Remove the user's UUID from the relay's Xray configuration (/usr/local/etc/xray/config.json) and restart Xray on the relay. This blocks the user at the transport layer.

Both steps are recommended

Removing either the SSH key or the UUID is sufficient to block access. However, performing both steps ensures defense-in-depth — the user is blocked at two independent layers.

Revocation takes effect on the next connection attempt. Existing active sessions are not terminated immediately; they will fail on reconnection.


Compliance Properties

Tunnel Whisperer's access control model supports the following compliance-relevant properties:

Zero plaintext data leaves the local network

All application data is encrypted by the SSH layer before it reaches the Xray tunnel. The TLS layer provides an additional envelope. No plaintext data is ever transmitted over the public internet.

No credentials stored on relay

The relay stores only Xray UUIDs (transport identifiers) and the public CA certificate(s) it trusts for admission. It does not store SSH private keys, CA signing keys, user passwords, or application data. Compromise of the relay does not expose user credentials or data.

Principle of least privilege

Each user can only forward traffic to the specific 127.0.0.1:<port> targets explicitly listed in their permitopen directives. There is no broad network access — every user is scoped to the minimum required services.

Audit trail via configurable logging

Tunnel Whisperer uses Go's slog structured logging framework. Log levels are configurable at runtime, and log output captures:

  • SSH authentication attempts (success and failure)
  • Port forwarding requests (allowed and denied)
  • Tunnel establishment and disconnection events
  • Xray connection lifecycle events

Log level configuration

The log level can be changed via the dashboard settings. Changing the log level triggers a server restart notification to ensure the new level takes effect.