Repository navigation
Shared Signals
An established VPN connection normally keeps running until OpenVPN asks for
reauthentication, for example at the next reneg-sec renegotiation. If the
identity provider ends a user's session in the meantime, the VPN connection
stays up.
With ssf.enabled, openvpn-auth-oauth2 acts as a receiver of the
OpenID Shared Signals Framework (SSF).
It polls the identity provider for
CAEP session-revoked
events and terminates the matching OpenVPN sessions within one poll interval.
Important
SSF requires the direct management interface.
The OpenVPN plugin can't terminate clients, so
openvpn-auth-oauth2 stops with an error when it connects to the plugin
with ssf.enabled.
- At startup,
openvpn-auth-oauth2reads the transmitter configuration metadata from the well-known path/.well-known/ssf-configurationof the issuer (see Other providers), fetches the transmitter's signing keys from itsjwks_uri, and reads the stream configuration from itsconfiguration_endpoint. The stream must already exist;openvpn-auth-oauth2doesn't create one. - It polls the stream's
endpoint_urlwith RFC 8936 poll delivery. - Each Security Event Token (SET) is verified: signature,
typ,iss,aud,iat, andjti. A token that fails verification is reported back to the transmitter as an error. Other event types, and subjects that can't be matched, are acknowledged and ignored. - The subject of a
session-revokedevent is matched against the OIDC identity every client logged in with. For every match,openvpn-auth-oauth2deletes the stored refresh token of the client (withoauth2.refresh.validate-user, it also revokes it at the provider) and, if the client is connected, sendsclient-kill <cid> HALTto OpenVPN. - The event is acknowledged only after the revocation succeeded. If it failed, the transmitter delivers the event again with a later poll.
At the first start, a discovery error stops openvpn-auth-oauth2, so a
misconfigured transmitter is noticed immediately. After a configuration reload
with SIGUSR1, a failed discovery doesn't affect VPN logins; it is logged and
retried in the background.
Errors while polling are logged and retried with an increasing delay of up to
5 minutes, or as long as the transmitter asks for with Retry-After, at most
1 hour. An event that can't be verified because the transmitter's signing keys
can't be fetched isn't acknowledged, so it is verified again with a later
poll.
| Event subject | Revoked OpenVPN sessions |
|---|---|
complex subject with a session member |
Sessions whose ID token had the same sid (and user, if present and supported). |
iss_sub subject, or complex with only a user member |
All sessions of the user, if ssf.user-level-revocation is true. |
email subject |
All sessions with that email address, if ssf.subject.allow-email and ssf.user-level-revocation are true. |
The issuer and subject of the event must equal the iss and sub claims of
the ID token the client logged in with, and the session must equal its sid
claim. A transmitter that names users with an issuer other than
oauth2.issuer matches no session. Keep ssf.user-level-revocation enabled
unless the transmitter always names a session. If the ID token has no sid
claim, only events that name the user can match.
Clients that connected without OIDC authentication, for example through
openvpn.bypass.common-names, aren't tracked and never revoked.
The identities are kept in memory. A configuration reload with SIGUSR1 keeps
them, but after a restart of openvpn-auth-oauth2, clients that are still
connected can only be revoked once they authenticate again, for example at the
next reneg-sec renegotiation.
HALT tells the client not to reconnect. A client that reconnects anyway
lands in the interactive browser login, because its refresh token is gone. The
identity provider has revoked the session itself, so the user must log in
again there.
If OpenVPN runs with auth-gen-token, use the external-auth flag:
auth-gen-token 28800 external-auth
Without external-auth, OpenVPN accepts a reconnecting client with a valid
auth token on its own and never asks openvpn-auth-oauth2, so a revoked client
could reconnect until its auth token expires. See
Non-interactive session refresh.
oauth2:
issuer: "https://keycloak.example.com/realms/example"
client:
id: "openvpn-auth-oauth2"
secret: "file:///etc/openvpn-auth-oauth2/client-secret"
ssf:
enabled: true
# issuer: "" # defaults to oauth2.issuer
poll:
# token: "" # defaults to a client credentials access token of oauth2.client
scopes: ["ssf.read"]
interval: 30s
max-events: 100
max-age: 1h
user-level-revocation: true
subject:
allow-email: false| Setting | Default | Description |
|---|---|---|
ssf.enabled |
false |
Receive SSF events and terminate revoked sessions. |
ssf.issuer |
oauth2.issuer |
Issuer of the SSF transmitter. Must use https://. It must equal the issuer in the transmitter metadata and the iss of every event. |
ssf.poll.token |
Static bearer token for the transmitter. If empty, openvpn-auth-oauth2 requests and renews an access token with the client credentials grant of oauth2.client. Supports file://. |
|
ssf.poll.scopes |
ssf.read |
Scopes of the client credentials access token. Ignored if ssf.poll.token is set. |
ssf.poll.interval |
30s |
Wait time between polls that return no events. Also the first retry delay after an error. Must be at least 1s. |
ssf.poll.max-events |
100 |
Maximum number of events per poll. |
ssf.max-age |
1h |
Events issued longer ago are rejected. Also the replay protection window. |
ssf.user-level-revocation |
true |
Revoke all sessions of a user for events without a session identifier. |
ssf.subject.allow-email |
false |
Accept events whose subject is an email address. Email addresses can be reassigned; prefer iss_sub. |
The poll endpoint, the audience, and the signing keys aren't configurable; they
come from the transmitter metadata and the stream configuration. The URLs of
the metadata, the signing keys, the stream configuration, and the poll endpoint
must use https://.
The client credentials grant authenticates like the login does: with
oauth2.client.secret, or with a private_key_jwt assertion if
oauth2.client.private-key is set. The OAuth2 client therefore needs the
client credentials grant (a service account) enabled at the provider. The
access token is reused until shortly before it expires, or until the
transmitter rejects it.
- Events are accepted only from the configured issuer and only if they're
signed with an asymmetric key from the transmitter's
jwks_uri. Keys are fetched again when an unknown key ID appears. - Events must be addressed to the audience of the stream.
- Every event ID (
jti) is processed only once withinssf.max-age. The replay protection is held in memory and starts empty after a restart or aSIGUSR1reload. -
openvpn-auth-oauth2only ends sessions; an SSF event can never start or extend one.
| Provider | session-revoked |
Poll delivery | Notes |
|---|---|---|---|
| Keycloak | Yes | Yes | Experimental feature, enable with --features=ssf. Polls always return immediately. |
| Okta | Yes | Not verified | Requires the ssf.read and ssf.manage scopes on the receiver app. |
| Google Workspace, Microsoft Entra ID | No | Neither offers a session-revoked transmitter for third-party receivers. |
Keycloak's SSF support is experimental and not recommended for production yet.
-
Start Keycloak with
--features=ssf. -
In Realm settings → SSF, enable the transmitter. The realm issuer
https://<host>/realms/<realm>is then also the SSF issuer, sossf.issuercan stay empty. -
Open the OIDC client used by
openvpn-auth-oauth2. Enable Client authentication and Service accounts roles, then switch on SSF enabled. -
In the Client scopes tab of the client, add the
ssf.readclient scope as Optional. Without it, Keycloak rejects the client credentials request withinvalid_scope. -
In the SSF → Receiver tab, allow Poll delivery, select the
session-revokedevent under Supported events, set Default subjects toALL, and keep the User subject formatiss_sub. WithNONE, Keycloak only sends events about users that were subscribed to the stream. -
In the SSF → Stream tab, create a stream with the delivery method Poll.
-
Configure
openvpn-auth-oauth2:ssf: enabled: true
The default scope
ssf.readis sufficient to read the stream and poll it.
Keycloak allows one stream per receiver client.
Keycloak sends session-revoked events with a complex subject that names the
user and the Keycloak session, so they end exactly the VPN connection that
logged in with that session. With the default oauth2.scopes,
openvpn-auth-oauth2 requests offline_access, so the session in the ID token
is an offline session. Tested with the Keycloak
nightly build of October 2026:
| Action in Keycloak | Effect |
|---|---|
| An administrator signs out the offline session of the VPN login. | The VPN connection of that session ends. |
| An administrator signs the user out of all sessions. | All VPN connections of the user end, if ssf.user-level-revocation is true. Keycloak sends the session ID ALL, which openvpn-auth-oauth2 treats as a user-level revocation. |
| Logging out of other applications. | No effect. The VPN login creates only the offline session, no browser single sign-on session. |
| Revoking the offline access of the client in the account console or the Consents tab. | No event; the VPN connection stays up until its next reauthentication. |
Events of other types, such as RISC account-disabled, are acknowledged and
ignored.
Any transmitter that implements SSF 1.0 with stream configuration and poll delivery works. The transmitter must:
- publish its metadata with the well-known path inserted between host and
path of the issuer, as SSF 1.0 requires
(
https://example.com/.well-known/ssf-configuration/tenantfor the issuerhttps://example.com/tenant), or appended to the issuer (https://example.com/tenant/.well-known/ssf-configuration), - return exactly one stream for the receiver from its
configuration_endpoint, with the delivery methodurn:ietf:rfc:8936, and - sign events with an asymmetric algorithm (RSA, RSA-PSS, ECDSA, or EdDSA).
With debug.metrics enabled, the following metrics are available on the
metrics endpoint:
openvpn_auth_oauth2_ssf_events_total{outcome}openvpn_auth_oauth2_ssf_sessions_revoked_total{scope}openvpn_auth_oauth2_ssf_poll_errors_totalopenvpn_auth_oauth2_ssf_last_successful_poll_timestamp_seconds
Alert if the last successful poll is older than a few poll intervals:
time() - openvpn_auth_oauth2_ssf_last_successful_poll_timestamp_seconds > 300
This wiki is synced with the docs folder from the code repository! To improve the wiki, create a pull request against the code repository with the suggested changes.
Start
Install and configure
- Installation
- Configuration reference
- OIDC providers
- HTTPS listener
- OpenVPN plugin
- Filesystem permissions
Identity and access
- Client token validation
- OpenVPN username
- CEL language features
- Client-specific configuration
- Non-interactive session refresh
- Shared Signals session revocation
Advanced integration and customization
Operate and troubleshoot
Learn and upgrade