Authentication

August 8, 2026 · View on GitHub

Xpra's authentication modules can be useful for:

  • securing socket connections
  • making the unix domain socket accessible to other users safely
  • using the proxy server

For more information on the different types of connections, see network. For more generic security information, please see security considerations

SSL mode can also be used for authentication using certificates (see #1252)

When using SSH to connect to a server, encryption and authentication can be skipped: by default the unix domain sockets used by ssh do not use authentication.


Starting with version 6.5, options for individual authentication modules are specified using brackets: `auth=MODULE(option=value,...)`. \ ie for starting a [seamless](Seamless.md) server with a `TCP` socket protected by a password stored in a `file`: ```shell xpra seamless --start=xterm -d auth --bind-tcp=0.0.0.0:10000,auth=file(filename=password.txt) ``` Multiple sockets can use different authentication modules, and those modules can more easily be chained: ```shell xpra seamless --start=xterm -d auth \ --bind-tcp=0.0.0.0:10000,auth=hosts,auth=file(filename=password.txt) \ --bind-tcp=0.0.0.0:10001,auth=sys ``` This is the recommended syntax, and the only one that is unambiguous: * the brackets delimit the module's options, so their values can contain `,` and `=` characters, which is common for command lines, paths and uris * the options belong to the module they follow, which matters when chaining several modules on a single socket

The older auth=MODULE:option=value and auth=MODULE,option=value forms are still accepted. The latter makes the option a socket option, which is then given to every authentication module used by that socket - so it cannot be used to give different values to two chained modules.

Server Authentication Modules

Xpra supports many authentication modules. Some of these modules require extra dependencies.

server authentication modules
ModuleResultPurpose
allowalways allows the user to login, the username used is the one supplied by the clientdangerous / only for testing
nonealways allows the user to login, the username used is the one the server is running asdangerous / only for testing
failalways fails authentication, no password requireduseful for testing
rejectalways fails authentication, pretends to ask for a passworduseful for testing
envmatches against an environment variable (XPRA_PASSWORD by default)alternative to file module
passwordmatches against a password given as a module option, ie: auth=password(value=mysecret)alternative to file module
multifilematches usernames and passwords against an authentication fileproxy: see password-file below
filecompares the password against the contents of a password file, see password-file belowsimple password authentication
scramSCRAM authentication using python-scrampsupports plaintext files and SCRAM stored-key records
pamlinux PAM authenticationLinux system authentication
win32win32security authenticationMS Windows system authentication
syssystem authenticationvirtual module which will choose win32 or pam authentication automatically
sqlitesqlite database authentication#1488
sqlsqlalchemy database authentication#2288
mysqlMySQL database authentication#2287
capabilitymatches values in the capabilities supplied by the client#3575
peercredSO_PEERCRED authentication#1524
hostsTCP Wrapper#1730
ratelimitdelays then rejects clients that keep failing to authenticatebrute force protection, chain it before a real authentication module
execDelegates to an external command#1690
kerberos-passwordUses kerberos to authenticate a username + password#1691
kerberos-tokenUses a kerberos ticket to authenticate a client#1691
gssUses a GSS ticket to authenticate a client#1691
oauthUses an OAuth2 Bearer token from websocket HTTP headers or client capabilitiesvalidates a static token or token introspection endpoint
keycloakUses a keycloak token to authenticate a client#3334
ldapUses ldap via python-ldap#1791
ldap3Uses ldap via python-ldap3#1791
u2fUniversal 2nd Factor#1789
fido2FIDO Alliance#1789
otpOne Time Passwordpyotp
otpscreenGenerates a one-time secret and shows it in a local GUI dialog for the user to typelocal secondary-channel confirmation (distinct from otp)
http-headervalidate websocket http headers#4438
more examples
  • XPRA_PASSWORD=mysecret xpra seamless --bind-tcp=0.0.0.0:10000,auth=env
  • SOME_OTHER_ENV_VAR_NAME=mysecret xpra seamless --bind-tcp=0.0.0.0:10000,auth=env(name=SOME_OTHER_ENV_VAR_NAME)
  • xpra seamless --bind-tcp=0.0.0.0:10000,auth=password(value=mysecret)
  • xpra seamless --bind-tcp=0.0.0.0:10000,auth=file(filename=/path/to/mypasswordfile.txt)
  • xpra seamless --bind-tcp=0.0.0.0:10000,auth=sqlite(filename=/path/to/userlist.sdb)
  • xpra seamless --bind-tcp=0.0.0.0:10000,auth=otpscreen(mode=alphanumeric,count=8,timeout=60)

Beware when mixing environment variables and password files as the latter may contain a trailing newline character whereas the former often do not.

The otpscreen module accepts the following options: mode (digits, alpha or alphanumeric, default digits), count (number of characters in the generated secret, default 6), timeout (how long the dialog stays up, in seconds, default 120), and display (which display to open the dialog on, default auto which reuses the server's saved DISPLAY / WAYLAND_DISPLAY).

rate limiting

The ratelimit module protects a socket against brute force attacks: it records how many times each client IP address has recently failed to authenticate, delays the ones that keep failing, and eventually rejects them outright.

It does not authenticate anyone by itself - it is a gate that must be chained before a real authentication module, and it must be listed first so that a blocked address is turned away before the server even sends it a challenge:

xpra start --bind-tcp=0.0.0.0:10000 \
  --tcp-auth=ratelimit(max-failures=3,window=60,ipv6-prefix=64) \
  --tcp-auth=password(value=mysecret)
OptionDefaultPurpose
max-failures5how many failures within the window are allowed before the client is rejected
window60how long a failure is remembered, in seconds
delay1delay added after the first failure, doubling with each one; 0 disables the delay
max-delay8upper limit for that delay, in seconds
ipv4-prefix32group IPv4 addresses by prefix, ie: 24 counts a whole /24 together
ipv6-prefix128group IPv6 addresses by prefix - 64 is recommended, see below
max-tracked10000how many addresses to remember at most

Once max-failures is reached, the client is rejected until the window expires: the rejected attempts are not counted again, so a legitimate user who gets locked out always recovers after window seconds.

An attacker usually controls an entire IPv6 subnet, so limiting each individual IPv6 address (the default) is easily bypassed by picking a new one for each attempt - use ipv6-prefix=64 to count a whole /64 together.

Loopback addresses, unix domain sockets and named pipes are never rate limited.

syntax for older versions

The syntax with older versions used a dedicated switch for each socket type:

  • --auth=MODULE for unix domain sockets and named pipes
  • --tcp-auth=MODULE for TCP sockets
  • --vsock-auth=MODULE for vsock (#983) etc

For more information on the different socket types, see network examples


By default, challenge-handlers=all which means that the python client will try all authentication handlers available until one succeeds. If the server is configured with multiple authentications modules for the same socket, the client will do the same.

Basic examples

Authenticating as username foo with password bar using the URI:

xpra attach tcp://foo:bar@host:port/

For a more secure option, storing the password value in a file, with debugging enabled:

echo -n "foo" > ./password.txt
xpra attach tcp://host:port/ --challenge-handlers=file:filename=./password.txt --debug auth
client challenge handlers
ModuleBehaviour and options
envname specifies the environment variable containing the password
defaults to XPRA_PASSWORD
filefilename specifies the file containing the passowrd
scramSCRAM password proof handler using python-scramp; legacy-sha1=yes enables SCRAM-SHA-1
gssuse gss-services to specify the name of the security context
kerberoskerberos-services specifies the valid kerberos services to connect to
the wildcard * may be used
promptGUI clients should see a dialog, console users a text prompt
u2fAPP_ID specifies the u2f authentication application ID
fido2APP_ID specifies the FIDO2 authentication application ID
uriUses values parsed from the connection string, ie: tcp://foo:bar@host

Password File

  • with the file module, the password-file contains a single password, the whole file is the password (including any trailing newline characters). To write a password to a file without the trailing newline character, you can use echo -n "thepassword" > password.txt
  • with multifile, the password-file contains a list of authentication values, see proxy server - this module is deprecated in favour of the sqlite module which is much easier to configure

Usernames

The username can be specified:

  • in the connection files you can save from the launcher
  • in the client connection string
tcp example
xpra attach tcp://username:password@host:port/

When an authentication module is used to secure a single session, many modules will completely ignore the username part, and it can be omitted from the connection string. This can be overriden for some modules.

example: specifying the password only

for connecting to the TCP socket and specifying the password only:

xpra attach tcp://:password@host:port/

Since the username is ignored, it can also be replaced with any string of your liking, ie using foobar here:

xpra attach tcp://foobar:password@host:port/

Only the following modules will make use of both the username and password to authenticate against their respective backend: kerberos-password, ldap, ldap3, sys (pam and win32), sqlite, sql, mysql, multifile and u2f. In this case, using an invalid username will cause the authentication to fail.

The username is usually more relevant when authenticating against a proxy server (see authentication details there).


The proxy server needs more than a yes/no answer from authentication: it also needs to know which xpra sessions the authenticated client may reach, and as which uid/gid to spawn (or connect to) the proxy instance.

Today, that lookup is bundled into the authentication module via the get_sessions() method on SysAuthenticatorBase, which returns a 5-tuple:

(uid, gid, displays, env_options, session_options)
  • uid, gid: the system identity the proxy instance runs as
  • displays: the list of display names the user may attach to (e.g. [":10", ":11"])
  • env_options: extra environment variables applied to the proxy instance process
  • session_options: extra session-level options passed to the proxy instance

The proxy server iterates over the protocol's authenticator chain after the challenge passes and uses the first non-empty result (see xpra/server/proxy/server.py).

The default implementation in SysAuthenticatorBase.get_sessions() performs a DotXpra socket-directory scan for the authenticated uid, listing every live xpra socket owned by the user. Most modules use this default (pam, ldap, ldap3, password, peercred, keycloak, kerberos-*, gss, u2f, fido2, otp, otpscreen, capability, env, exec, hosts, http-header, allow, none, win32, file).

Three families override it to return data they already store per user:

ModuleSource of session data
multifileExtra columns in the password file (see the multifile format in Proxy-Server.md)
sqliteColumns uid, gid, displays, env_options, session_options of the users table (see xpra/auth/sqlauthbase.py schema)
sqlSame schema, via SQLAlchemy
mysqlSame schema, against MySQL

The fail and reject modules deny authentication outright and therefore never reach session lookup.

The --session-registry proxy option

The proxy server lets you pick the session registry independently of the authenticator with --session-registry=NAME[(opt=val,...)] (default auth):

RegistryBehaviour
authDelegates to authenticator.get_sessions() — the historical behaviour. multifile/sql* setups need no changes.
socketPerforms a DotXpra socket-directory scan for the authenticated uid — pairs any authenticator with socket discovery.
multifileReads username|password|uid|gid|displays|env|session_options from a file (filename option). Lookup is by username.
sqliteLooks up (uid, gid, displays, env_options, session_options) from the users table of an sqlite database (filename option).
sqlSame schema, via SQLAlchemy (uri option).
mysqlSame schema, against MySQL (uri option).
liveRuntime map of sessions populated by xpra servers that dial out to the proxy at startup with --register=URI. See below.

Example: use pam to authenticate but read the per-user session mapping from an sqlite file:

xpra proxy --bind-tcp=0.0.0.0:14500,auth=pam --session-registry=sqlite(filename=/etc/xpra/users.sdb)

Registry modules live under xpra/server/session_registry/.

The live backend and --register

A server can announce itself to a proxy at startup with the --register=URI option (repeatable). For each URI the server dials the proxy, authenticates as a client, and sends a hello packet carrying request=register along with its uuid, session-name and display.

# proxy side:
xpra proxy --bind-tcp=0.0.0.0:14500 --session-registry=live --auth=password(value=secret)

# server side (--session-name names the registered session):
xpra seamless --start=xterm --session-name=demo --register=tcp://:secret@proxy.example.com:14500/

The proxy exposes the registered sessions under the registered key in xpra info.

Picking a session from the client

To address a specific registered session, pass --display=NAME to xpra attach:

xpra attach tcp://proxy.example.com:14500/ --display=demo

When exactly one session is registered, xpra attach tcp://proxy.example.com:14500/ is enough — the proxy auto-selects it.

By default the name supplied by --display is matched against each registered session's session-name (and then its registered displays). The match policy can be changed on the proxy with the lookup-by option (session-name, uuid or display).

The proxy never dials out — it only ever accepts inbound connections, which makes it usable in front of NAT-ed servers. After each client is brokered, the server re-registers automatically so the slot stays warm for the next one.


Authentication Process

The steps below assume that the client and server have been configured to use authentication:

  • if the server is not configured for authentication, the client connection should be accepted and a warning will be printed
  • if the client is not configured for authentication, a password dialog may show up, and the connection will fail with an authentication error if the correct value is not supplied
  • if multiple authentication modules are specified, the client may bring up multiple authentication dialogs
  • how the client handles the challenges sent by the server can be configured using the challenge-handlers option, by default the client will try the following handlers in the specified order: uri (whatever password may have been specified in the connection string), file (if the password-file option was used), env (if the environment variable is present), scram, kerberos, gss, keycloak, u2f and finally prompt
module and platform specific notes
  • this information applies to all clients except the HTML5 client: regular GUI clients as well as command line clients like xpra info
  • each authentication module specifies the type of password hashing it supports (usually HMAC)
  • some authentication modules (pam, win32, kerberos-password, ldap and ldap3) require the actual password to be sent across to perform the authentication on the server - they therefore use the weak xor hashing, which is insecure
  • you must use encryption to be able to use xor hashing so that the password is protected during the exchange: the system will refuse to send a xor hashed password unencrypted
  • encryption is processed before authentication
  • when used over TCP sockets, password authentication is vulnerable to man-in-the-middle attacks where an attacker could intercept the initial exchange and use the stolen authentication challenge response to access the session, encryption prevents that
  • the client does not verify the authenticity of the server, using encryption effectively does
  • enabling auth debug logging may leak some authentication information
  • if you are concerned about security, use SSH as transport instead

For more information on packets, see network.

Writing a new authentication module

A new server-side authentication module is a Python file in xpra/auth/ that defines a class named Authenticator. Two base classes are provided:

  • SysAuthenticatorBase — the minimal base. Use this when the username does not need to map to a local system account (e.g. token-based or capability-based authenticators).
  • SysAuthenticator (in the same file) — extends the base by loading the local pwd entry for self.username on POSIX. Use this when the module is tied to system users (pam, peercred, exec, etc.).

The methods most commonly overridden:

MethodDefaultWhen to override
requires_challenge()returns TrueReturn False for modules that authenticate out of band (e.g. peercred, hosts, http-header).
get_challenge(digests)Generates a salt and chooses the strongest compatible digestOverride when the module requires a named non-HMAC digest or custom challenge payload.
get_next_challenge()returns ()Override for multi-step challenge protocols. Return (challenge, digest, prompt) while another client response is needed.
get_passwords() / get_password()get_passwords returns (get_password(),); get_password returns ""Override one of them to provide the expected password(s) — used by HMAC challenge verification.
do_authenticate(caps)Validates the challenge response and calls authenticate_checkOverride for non-HMAC flows (e.g. challenge/response over a different transport, third-party token verification).
authenticate_hmac(caps)Verifies the HMAC challenge against get_passwords() resultsOverride if you need to perform extra checks after a successful HMAC match.
get_uid() / get_gid()NotImplementedErrorAlways override. Return the uid/gid the proxy instance should run as. Use parse_uid / parse_gid from common.py.
get_sessions()Performs a DotXpra socket scan for the authenticated uidLeave alone unless your backend stores per-user session metadata (see multifile and sqlauthbase.py for examples).

Helpers in xpra/auth/common.py:

  • SessionData — the (uid, gid, displays, env_options, session_options) 5-tuple returned by get_sessions()
  • parse_uid(v) / parse_gid(v) — accept either a numeric string or a username/group name, with safe defaults
  • get_auth_exec_env(display="auto") — environment dictionary suitable for spawning helper processes (used by exec and otpscreen)

Authenticator instances are constructed by auth_helper.get_auth_module(), which parses the auth=NAME(opt=value,...) syntax and imports xpra.auth.<name>. Each socket can chain multiple authenticators; the first one to require a challenge issues it and subsequent ones either verify additional caps or contribute to get_sessions().

Multi-step authenticators keep their state on the Authenticator instance. After authenticate(caps) succeeds, the server calls get_next_challenge(): return () when the authenticator is complete, or return (challenge_bytes, digest_name, prompt) to send another challenge packet. The next client response arrives in caps["challenge_response"] and is processed by the same authenticator.

Three optional callbacks are called on the authenticators of a connection, if they are defined:

CallbackCalled when
auth_failed()any module in the chain rejected the client - an authenticator only ever sees its own result, this is how it can find out that a later module failed
auth_succeeded()every module in the chain has passed
cleanup()the authenticators are discarded (on success and on failure), to free up any resources

A new Authenticator is instantiated for every connection, so a module that needs to remember something across connections (like ratelimit, which counts the failures of each client IP) must keep that state at the class level and protect it with a lock: verify_auth runs in a separate thread for each connection.

Writing a new client challenge handler

A new client-side challenge handler is a Python file in xpra/challenge/ that defines a class named Handler implementing AuthenticationHandler.

MethodDefaultWhen to override
get_digests()abstractReturn the digest names handled by this module, or () for generic password handlers.
handle(challenge, digest, prompt)abstractReturn the response bytes/value for the server challenge, or a false value if the handler cannot answer.
is_done()returns TrueReturn False for multi-step handlers that must keep state and handle the next challenge packet.

For multi-step handlers, keep protocol state on the handler instance. When handle() returns a response and is_done() is False, the client keeps that handler at the front of the handler list so the next server challenge is routed back to it.

Salt handling is important
  • 64-bit entropy is nowhere near enough against a serious attacker: If you want to defend against rainbow tables, salts are inevitable, because you need a full rainbow table per unique salt, which is computationally and storage-wise intense
  • SHA-512 w/ per User Salts is Not Enough: In the event the hash was disclosed or the database was compromised, the attacker will already have one of the two values (i.e. the salt), used to construct the hash
  • about hmac: _Those people should know that HMAC is as easy to precompute as naked SHA1 is; you can "rainbow-table" HMAC_* and we did get it wrong before...