Authunnel is an authenticated tunnel for reaching private TCP services, including SSH, through an OAuth2-protected TLS WebSocket conduit.
An Authunnel server acts as an "SSH Bastion": it exposes a TLS-secured, OIDC-authenticated port, through which clients can reach internal SSH hosts that are otherwise firewalled from the internet.
The typical workflow is:
sshlaunches the Authunnel client asProxyCommand.- The client reuses a cached token, refreshes it, or completes Authorization Code + PKCE in a browser.
- The Authunnel server, acting as an OAuth2 resource server, locates the issuer's JWKS endpoint (by OIDC discovery unless configured otherwise) and validates the JWT access token locally.
- The server opens the requested
%h:%pdestination. - SSH stdio is bridged over that authenticated path.
Authunnel authenticates the network path to a private service: it decides, using OIDC, who may open a tunnel to a destination. It does not provide OIDC-authenticated SSH login. Once the tunnel is open, SSH performs its own host and user authentication (keys, certificates, passwords) exactly as it would over any other transport. Authunnel never sees or brokers your SSH credentials, and a valid OIDC token does not by itself grant a login to any host.
In other words, Authunnel provides bastion-like functionality — a single authenticated, audited entry point to an internal network — rather than federating SSH login to your identity provider.
If you want OIDC identity to issue the SSH credential itself, use a purpose-built tool built on open source and open standards, optionally alongside Authunnel:
- opkssh / OpenPubkey — embeds an OIDC ID token in the SSH public key so OpenSSH can authenticate the user against their IdP (github.com/openpubkey/opkssh).
- Smallstep
step-cawith its OIDC provisioner — an open source SSH certificate authority that issues short-lived SSH user certificates after an OIDC browser login (smallstep.com/docs/step-ca).
These compose naturally with Authunnel: OIDC governs the tunnel (network admission and audit) while an OIDC-backed SSH CA governs the login on the host at the far end.
- docs/DEPLOYMENT.md — running the server: TLS modes, reverse-proxy configuration, the full server flag reference, egress policy, OIDC client registration, and the deployment hardening checklist. Also two things that span both binaries: the transport rules on the auth path, and protected-resource metadata, which is how a server can let a client be configured with nothing but
--tunnel-url. - docs/DEVELOPMENT.md — building and testing from source: codebase layout, auth-flow invariants, the test suite, and the local Keycloak environment.
- examples/ — runnable CloudFormation templates that stand up a complete Authunnel topology on AWS, so you can see it working end to end before deploying on your own infrastructure.
server/server.go- HTTPS server on a configurable listen address (default
:8443for TLS-files,:443for ACME,:8080for plaintext-behind-reverse-proxy) - Conservative HTTP server timeouts to reduce slow-client resource exhaustion risk
- Structured JSON logs with three correlation IDs:
request_id— generated per HTTP request; scoped to a single request/response cycletrace_id— extracted from an incomingTraceparentheader (W3C Trace Context) when present, otherwise generated; allows correlation with upstream infrastructure such as a load balancer or reverse proxytunnel_id— generated when a WebSocket upgrade succeeds; scoped to the lifetime of the SOCKS tunnel and inherited by all subsequent tunnel events (open, SOCKS CONNECT, close)
- Tunnel logs include the authenticated user identity, with per-destination SOCKS CONNECT logs at debug level; all three correlation IDs are carried through so HTTP admission, tunnel lifecycle, and per-destination events can be joined
- OAuth2 resource-server JWT validation: the issuer's JWKS endpoint is located once at startup; claim checks run locally against a cached key set, refreshed only when a token presents an unknown key ID
- Optional admission controls: global and per-user concurrent-tunnel caps, per-user tunnel-open rate limit, and a bounded dial timeout for outbound SOCKS CONNECT
- WebSocket tunnel endpoint (
/protected/tunnel) connected to an in-process SOCKS5 server
- HTTPS server on a configurable listen address (default
client/client.go- ProxyCommand mode: stdio bridge for direct SSH integration
- Unix socket mode: local SOCKS5 endpoint for generic client tooling
- Managed OIDC mode: public-client PKCE login with token cache + refresh, configured from the server's RFC 9728 protected-resource metadata unless overridden by flags
- Control-message listener for server-initiated longevity warnings; automatic token refresh when the server signals imminent token expiry
- Reads OIDC issuer, audience, listen address, TLS mode, and connection longevity configuration from flags or environment.
- Locates the issuer's JWKS endpoint once at startup — by OIDC discovery unless configured otherwise. The mode in effect is reported as
discovery_modeon thetoken_validator_readystartup log line; see docs/DEPLOYMENT.md for the three modes and their trade-offs. - Publishes RFC 9728 protected-resource metadata at
GET /.well-known/oauth-protected-resource, unauthenticated, naming the issuer it validates against so clients need no OIDC configuration of their own. Disabled by--no-resource-metadata. - Accepts
GET /protected/tunnel, verifies the bearer token's signature, issuer, expiration, audience, subject presence,iatsanity, andnbf(the token must be usable now at admission), then checks the WebSocket upgrade headers. Per RFC 6750 §3.1, a missing credential and a failed token both get401— the latter witherror="invalid_token"— while403is reserved for the origin check, since this server enforces no scopes. Other HTTP methods receive405from the router. - Applies admission controls (concurrent-tunnel caps and per-user rate limits) when configured, rejecting over-limit requests with
429/503and aRetry-Afterheader. - Upgrades the connection to WebSocket.
- Hands each upgraded connection to the SOCKS5 server implementation.
- If connection longevity is configured, manages tunnel lifetime: warns clients before expiry and disconnects when limits are reached. When token-expiry enforcement is active, the server accepts refreshed tokens from the client to extend the tunnel.
- Emits structured JSON logs for request lifecycle, auth failures, tunnel open/close events, token refresh outcomes, and debug-level SOCKS CONNECT destinations.
- Either:
- uses a bearer token supplied via the
ACCESS_TOKENenvironment variable, or - runs managed OIDC mode, which is the default whenever
ACCESS_TOKENis unset.
- uses a bearer token supplied via the
- In managed mode, missing OIDC values are read from the tunnel server's RFC 9728 protected-resource metadata, so
--tunnel-urlon its own is a complete configuration provided the server publishes a client ID. The lookup runs only when an essential value is absent — the client ID, or both the issuer and metadata URL — and never on a cache hit, so a client that supplies those makes no extra request and adopts no published hints either. A value you pass always wins over the published one. See docs/DEPLOYMENT.md. - In managed mode the client:
- reuses a cached token when it remains valid for more than 60 seconds,
- otherwise refreshes it when a refresh token is available,
- otherwise launches a browser to the IdP and listens on
127.0.0.1for the callback.
- The client opens an authenticated WebSocket connection to the Authunnel server.
- A background control-message listener handles server-initiated longevity messages. When the server warns that the access token is about to expire, the client automatically obtains a fresh token (via its existing refresh logic) and sends it to the server to extend the tunnel.
- In ProxyCommand mode it performs a SOCKS5 CONNECT for
%h:%pand bridgesstdin/stdout. - In unix-socket mode it exposes a local SOCKS5 endpoint and opens a dedicated tunnel per local connection.
Authunnel is deliberately simple in both functionality and implementation — a small, focused codebase that is intended to be easy to read and audit in full. Complexity is kept low by design; if a feature would make the security model harder to reason about, that is a reason not to add it.
The following properties are enforced by default with no silent bypass. Where a development override exists it is noted explicitly:
- Bearer token validation at the WebSocket layer before any SOCKS5 connection can be attempted: signature, issuer, audience (
aud), expiry (exp), non-empty subject (sub), not-before (nbfmust be usable at admission time, with a 30-second clock-skew allowance), and sane issued-at (iatmust not be meaningfully in the future). The bearer token is length-capped at 8 KiB and theAuthorizationheader at 8 KiB + 64 bytes before the verifier runs, so anonymous callers cannot push oversized payloads onto the JWT parser. Thehttp.Serverrequest-header memory cap is also lowered from Go's 1 MiB default to 16 KiB as a defence-in-depth boundary against oversized non-bearer headers. - Bounded issuer metadata and JWKS fetches: both the server-side validator and the managed client use conservative dial, TLS-handshake, response-header, and overall timeouts. Derived and overridden metadata discovery is bounded during startup.
--oidc-jwks-uriskips discovery so server egress can be restricted to the key endpoint; the first authenticated request and later key rotation still require that endpoint. See Issuer metadata and key discovery. - No internal-address pivot from discovered configuration: an address named by a remote metadata document — the issuer, the authorization-server metadata URL, or an endpoint it advertises — is refused when it resolves into the built-in protected set (loopback, IPv4/IPv6 link-local including cloud IMDS, unspecified, multicast). Private networks are deliberately not refused, and the guard applies only to values the tunnel server chose. Enforced at three layers. Behind a forward proxy the two connection-level layers do not apply to an
httpsdestination — this client makes no connection of its own, so certificate verification takes over, and that binds the hostname rather than establishing the address is external. The static resolution check still runs, and still refuses a name local DNS resolves into the protected set. The conditions that model rests on, and what--oidc-issuerdoes and does not answer, are in Discovered addresses and the internal-address guard. - No transport downgrade on the auth path: neither side will fetch credentials or signing keys over a weaker transport than the metadata that named them. An
httpsmetadata document may not advertise plaintext endpoints, and redirects may not move a metadata, JWKS, or token fetch offhttps. Advertised endpoints must behttp(s)URLs with a host in every mode, including under--insecure-oidc-issuer— this is what keeps a scheme likefile://out of the authorization URL that the client hands to the OS. Between two HTTPS origins a metadata document may be delegated freely, but a token request may not change origin at all: a 307 preserves the body, so that hop would forward a refresh token or an authorization code. A metadata fetch is pinned the same way in the one case where an origin was promised — see What--oidc-issuerpins, precisely. On the client, a refusal is terminal rather than falling back to an interactive login that would fail the same way. The PKCE loopback callback stays plaintext by design (RFC 8252). Details in docs/DEPLOYMENT.md. - Subject pinning during token refresh: the server rejects any refreshed token whose
subdiffers from the original tunnel's subject. - Refresh deadline enforcement: a refreshed token whose
nbffalls after the current enforced connection deadline (exp + --expiry-grace) is rejected. A refresh handover cannot silently extend the policy beyond what the operator has opted into. The comparison is strict — no additional clock-skew allowance applies beyond--expiry-grace. - Secure transport by default: the OIDC issuer URL must be
https://; the client's tunnel endpoint URL must behttps://orwss://. Plaintext variants require explicit override flags (see the development-only overrides in the server flag reference). - Explicit egress posture at startup: the server refuses to start without either
--allowrules or--allow-open-egress. This prevents a misconfigured deployment from silently becoming an open TCP pivot.
By default a client takes the authorization server's identity from the tunnel server
it is connecting to. That is what lets --tunnel-url be a complete configuration when
the server publishes a client ID, and it is a real change in what the tunnel URL
controls:
- What it removes. Every value an operator would otherwise transcribe into an
ssh_configline per user, per host block — and with it the class of failure where a wrong issuer or client ID survives a browser login and comes back as an uninformative401from the tunnel server. - What it costs. A hostile
--tunnel-urlcan name an authorization server of its choosing and so send the user to a credential-phishing page. Note the starting point: whoever controls that URL already receives every access token this client obtains, so the escalation is from harvesting the tunnel's token to phishing the IdP password — serious, but from a position that is already lost. - What bounds it. The document is fetched from the tunnel URL's own origin, over
the transport that URL specifies, and it must declare exactly the identifier the
client is using — scheme, host, path and query — so a host serving several protected
resources cannot hand one client another resource's configuration. Under
httpsthat means only the holder of a certificate for the tunnel host can supply it. The addresses such a document names are also held to the resolved-IP deny-list: a tunnel server reached over a public address may not aim the client's own auth traffic at loopback, a link-local address, or a cloud instance-metadata service. Discovery can also only ever cause an interactive login to a newly-named authorization server, which the user sees — never a silent hand-off of an already-issued refresh token, because a cached credential whose recorded issuer, client, or metadata URL no longer matches is discarded rather than reused. - How to decline it. Pass
--oidc-issuerand the client refuses a document naming a different one — and refuses to let the tunnel server relocate that issuer's metadata off its own origin, without which the pin would be hollow. It does not pin what the token is for, so add--oidc-audienceor--oidc-resourceif you do not trust the tunnel URL, and--no-resource-metadatato refuse the lookup as a standing rule rather than as a side effect of having configured everything. Exactly what each of those pins: What--oidc-issuerpins.
The following are disabled or unlimited by default and must be explicitly configured for a hardened deployment. docs/DEPLOYMENT.md covers each in detail and ends with a hardening checklist to verify before going to production:
- Egress allowlist (
--allow): limits the destinations authenticated clients may reach. Recommended for production; restricts the blast radius if a credential is compromised. - Egress open mode (
--allow-open-egress): explicit opt-in to allow any destination reachable by the server process. Logged at warn level on startup. Mutually exclusive with--allow. - Resolved-IP deny-list (
--ip-block,--no-ip-block): on by default with a built-in protected set — loopback, IPv4/IPv6 link-local (incl. cloud IMDS169.254.169.254), unspecified, and multicast. Applied independently of the egress posture: the deny-list runs after the allow check in both restrictive and open modes, so a hostname rule that resolves to a protected address is rejected regardless. RFC1918, CGNAT, and IPv6 ULA are not in the default set.--ip-blockreplaces the default with an operator-supplied list (CIDR, bare IP, or bracketed IPv6);--no-ip-blockdisables the guard entirely. - Connection longevity (
--max-connection-duration,--expiry-grace,--no-connection-token-expiry): by default tunnel lifetime is tied to the access token'sexp. These flags let operators tune for specific IdP behaviors or impose hard ceilings. Some IdPs (e.g. Auth0) cache access tokens;--expiry-graceextends the enforcement deadline beyondexpto give the client time to obtain a genuinely new token. - Admission limits (
--max-concurrent-tunnels,--max-tunnels-per-user,--tunnel-open-rate,--dial-timeout): zero or default by default. Configure for production to bound resource use and prevent a single credential from monopolising tunnel capacity or tying up goroutines on blackholed destinations. - Published client configuration (
--client-id,--client-default-scopes,--client-audience,--client-resource,--client-oidc-metadata-url,--no-resource-metadata): the protected-resource document is published by default and names the issuer; the hints that let a client run with no OIDC flags at all are opt-in per value. Setting them is what turns--tunnel-urlinto a complete client configuration. See the trade described above before deciding. - Pre-auth IP rate limit (
--preauth-rate,--preauth-burst): off by default, matching the explicit-posture style of the egress flags. When enabled, runs before bearer-token parsing on every authenticated route (/protected,/protected/, any/protected/*, and/protected/tunnel) so a flood of anonymous or junk-JWT requests is rejected with429before any validator or JWKS work happens. Recommended for direct internet exposure; deployments behind a load balancer that already rate-limits anonymous traffic can leave it off. Buckets key on the TCP peer by default;--preauth-trust-forwarded-foropts into trustingX-Forwarded-Forbehind a known proxy (see docs/DEPLOYMENT.md).
- Live token revocation: revoking a token at the IdP does not terminate an already-established tunnel. Authunnel enforces token expiry but does not perform per-request introspection checks.
- Tunnel chain observability: Authunnel can only log and control connections it directly brokers. A client could SOCKS CONNECT to a second tunnel or proxy, creating a chain Authunnel cannot observe.
- OIDC-authenticated SSH login: Authunnel authenticates the tunnel, not the SSH session. It does not federate host login to your IdP — see Scope: tunnel authentication, not SSH login for the distinction and for open-source tools that do.
- Session architecture redesign: the current WebSocket-to-SOCKS model is intentionally simple and is not expected to change.
- Released
authunnel-serverandauthunnel-clientbinaries for your platform. Go is only needed if you build from source or run thego runexamples below. - An OIDC provider that issues JWT access tokens carrying both a server audience (emitted as
aud) and a non-emptysub— Authunnel pins each tunnel's refresh identity tosub, so tokens without one are rejected at admission. Most IdPs emitsubby default; on Keycloak 26+ the client's default scopes must cover it (the built-inbasicscope, or an equivalent custom scope with anoidc-sub-mapper— seetestenv/keycloak/authunnel-realm.jsonfor a working example) - A TLS certificate trusted by the client runtime (for TLS-files mode; not required for ACME or plaintext-behind-reverse-proxy modes)
The server runs on Linux and macOS. The client runs on Linux, macOS, and Windows (10 1803 or later).
The examples below use go run from a source checkout. When using released
binaries, invoke authunnel-server or authunnel-client with the same flags
and environment variables.
Server setup is covered in full in docs/DEPLOYMENT.md — TLS modes (certificate files, ACME / Let's Encrypt, plaintext behind a reverse proxy), the complete flag reference, and egress policy. A minimal example using TLS certificate files:
export OIDC_ISSUER='https://<issuer>'
export TOKEN_AUDIENCE='authunnel-server'
export TLS_CERT_FILE='/etc/authunnel/tls/server.crt'
export TLS_KEY_FILE='/etc/authunnel/tls/server.key'
cd server && CGO_ENABLED=0 go run . --allow '*.internal:22'Egress posture is required at startup. Either pass one or more --allow rules (recommended) or pass --allow-open-egress to explicitly opt into open mode. Running without either is rejected — see the "Security Posture" section above.
This is the intended ssh workflow.
Example SSH config entry:
Host internal-host
HostName internal-host
User myuser
ProxyCommand /path/to/authunnel-client \
--tunnel-url https://localhost:8443/protected/tunnel \
--proxycommand %h %pThe issuer, client ID, scopes and audience come from the server. Pass them explicitly
(--oidc-client-id, and either --oidc-issuer or --oidc-metadata-url, …) if the
server is run with --no-resource-metadata, or to override what it publishes.
On Windows with OpenSSH, use the full path with backslashes and quote it if it contains spaces:
Host internal-host
HostName internal-host
User myuser
ProxyCommand "C:\path\to\authunnel-client.exe" --tunnel-url https://... --proxycommand %h %pUseful client flags. Every OIDC flag is optional — each overrides the corresponding value the tunnel server publishes, and is needed only when the server publishes nothing:
--oidc-issuer--oidc-client-id--oidc-metadata-urlto use trusted authorization-server metadata at a URL other than the path derived from--oidc-issuer. Usable with or without--oidc-issuer; without one the document's own issuer is adopted, which trades away the consistency check. Changing it discards the token cache and requires a fresh login. See Issuer metadata and key discovery--oidc-audienceto send the Auth0-styleaudienceparameter during managed login--oidc-resourceto send the RFC 8707resourceparameter during managed login; required by providers that bind the tokenaudto a requested resource, such as AWS Cognito--oidc-redirect-portto use a fixed loopback callback port instead of a random one--oidc-scopes, defaulting to the scopes the tunnel server publishes, oropenid offline_accesswhen it publishes none--oidc-cachewith default${XDG_CONFIG_HOME:-~/.config}/authunnel/tokens.json(macOS/Linux) or%AppData%\authunnel\tokens.json(Windows)--oidc-no-browserto print the URL without attempting automatic browser launch--no-resource-metadatato refuse the tunnel-server lookup outright.--oidc-client-idand one of--oidc-issuer/--oidc-metadata-urlare then required, and a missing one is a startup error rather than a failure at first use. Distinct from simply supplying everything, which also results in no lookup: this is a prohibition that survives someone shortening theProxyCommandline, and it is visible in that line. Same flag name as the server's, same document, opposite direction — there it means do not publish, here it means do not read--tunnel-url— tunnel endpoint URL. Secure schemeshttps://andwss://are accepted by default; plaintexthttp://andws://require--insecure-tunnel-url. Required. May also be supplied via theAUTHUNNEL_TUNNEL_URLenvironment variable (the flag takes precedence)--unix-socket--proxycommand--insecure-oidc-issuer— allow non-HTTPS OIDC issuer and metadata URLs, discovered as well as configured (development only; do not use in production). Relaxes transport security only; other schemes such asfile://stay rejected--insecure-tunnel-url— allow a non-HTTPS tunnel endpoint URL (development only; do not use in production)
On first use the client prints the authorization URL to stderr and tries to open the system browser. Subsequent runs reuse the cache or refresh token when possible.
The IdP-side client registration required for managed mode (public client, PKCE, loopback redirects, refresh tokens) is described in docs/DEPLOYMENT.md.
A pre-obtained bearer token can be supplied via the ACCESS_TOKEN
environment variable. This is mutually exclusive with all managed OIDC
flags. There is no command-line equivalent: bearer tokens passed as
arguments would be visible via process listings and shell history.
The examples below source the token from a secrets manager so the literal
value never appears in shell history or argv. Substitute whichever helper
you use (pass, vault kv get, op read, security find-generic-password -w, gpg --decrypt, etc.); the goal is that the token comes from outside
the typed command line.
# The ACCESS_TOKEN= prefix scopes the value to this single client
# invocation; it is not exported to the shell. Avoid
# `export ACCESS_TOKEN=<literal>`, which writes the token to shell history.
cd client
ACCESS_TOKEN="$(pass show authunnel/access-token)" \
CGO_ENABLED=0 SSL_CERT_FILE=../cert.pem go run . \
--tunnel-url https://localhost:8443/protected/tunnel \
--unix-socket /tmp/authunnel/proxy.sockProxyCommand example, same pattern:
ACCESS_TOKEN="$(pass show authunnel/access-token)" /path/to/authunnel-client \
--tunnel-url https://localhost:8443/protected/tunnel \
--proxycommand internal-host 22If you already export ACCESS_TOKEN from a wrapper script or a
shell-startup integration with your secrets manager, you can omit the
inline substitution and just invoke the client directly.
cd client
CGO_ENABLED=0 SSL_CERT_FILE=../cert.pem go run . \
--tunnel-url https://<host>:8443/protected/tunnel \
--unix-socket /tmp/authunnel/proxy.sockUse with socat in an SSH ProxyCommand:
Host internal-host-via-socat
HostName internal-host
User myuser
ProxyCommand socat - SOCKS5:/tmp/authunnel/proxy.sock:%h:%pIf the unix-socket parent directory does not already exist, the client creates
it with 0700 permissions. It also tightens the socket itself to 0600 so
other local users cannot connect by default on shared hosts.
On shared POSIX hosts the client fails closed if the socket's parent directory
is group- or world-writable, or if it is owned by another local user. It also
walks every ancestor up to the filesystem root: any ancestor directory a peer
can rename(2) past would let them swap the private subtree between
validation and bind, so ancestors that are writable by others without the
sticky bit, or owned by an unprivileged user other than the operator, are
rejected too. Sticky directories (the classic case is /tmp, mode 1777)
are accepted as ancestors because sticky-bit semantics restrict renames to
the entry's owner — but the leaf must still be a private subdirectory (for
example /tmp/authunnel/, mode 0700), so point --unix-socket at a file
inside it rather than directly at /tmp/proxy.sock. A bare filename like
--unix-socket proxy.sock is validated against the current working
directory under the same rules, so starting the client from a shared cwd
(such as /tmp itself) is refused. The same checks apply to the OIDC token
cache directory (--oidc-cache) and its advisory-lock companion file, so a
directory that is safe for the socket is also safe for cached tokens.
Managed OIDC mode writes the cached access token and refresh token to
--oidc-cache as plaintext JSON. Confidentiality on disk is enforced by
POSIX filesystem permissions alone: the cache file is created 0600 via
atomic rename, inside a 0700 directory whose ancestors have been
validated against peer rename(2) as described above.
The client also re-validates an existing cache file before reading it, so
a tokens.json left over from another tool with 0o644 (or any
group/world bit), with a foreign owner, or replaced by a symlink is
rejected with a validate OIDC token cache: startup error rather than
silently honoured. The fix is one of chmod 600 ~/.config/authunnel/tokens.json (POSIX) or deleting the file and
re-authenticating; the validator deliberately does not auto-chmod, so
the audit signal is preserved.
This design matches the pattern used by most OIDC CLIs, but operators should be explicit about what it does and does not defend against:
- Defended: read access by other unprivileged users on the same host, including concurrent attackers who can observe the config directory but not write into it.
- Not defended: the machine's root user, offline forensic access to an unencrypted disk or disk image, backups of the user's config directory, or any process running as the same uid (which by construction already has the same tokens available through the authunnel client itself).
If your threat model requires stronger at-rest protection, either run
authunnel on a system with full-disk encryption (so offline disk access is
excluded), or supply the access token directly via ACCESS_TOKEN from a
secrets manager so no refresh token is ever persisted by authunnel.
During listener creation the client restricts its process umask to 0o077,
so the socket inode is created owner-only in the first place; the follow-up
chmod to 0600 is kept as a safety net for filesystems that ignore umask
on AF_UNIX bind. Stale-socket cleanup after a previous crash refuses to
remove anything other than a unix-domain socket owned by the current user,
so a regular file accidentally placed at the socket path will surface as an
error rather than being silently unlinked.
Unix socket mode works on Windows 10 1803 and later. Windows uses NTFS ACLs
rather than POSIX mode bits, so the parent-directory safety check there only
verifies that the target path exists as a directory; detailed ACL inspection
is out of scope and operators should rely on the default %AppData%
location, which is already user-scoped.
Authunnel follows Semantic Versioning. A new major version may introduce breaking changes to configuration flags, environment variables, or the wire protocol. Check the release notes before upgrading across a major version boundary.
Release assets are accompanied by SHA-256 checksums and GitHub artifact attestations. After downloading an asset, verify its provenance with:
gh attestation verify authunnel-client-linux-amd64 -R dkj/authunnelSee LICENSE.