Protect XRDP and GNOME Access With Entra ID Credentials and Rublon MFA
Use Microsoft Entra ID for primary password authentication and require Rublon MFA before users can access or unlock Debian desktop sessions.
Use Microsoft Entra ID Credentials for XRDP and GNOME Logins
Add MFA to XRDP Remote Desktop Access
Add MFA to GDM and GNOME Screen Unlock
Extend an Entra-First Identity Strategy to Debian Desktops
What This XRDP and GNOME Integration Does
alice
alice@example.com
How XRDP and GNOME Login With Microsoft Entra ID Credentials and MFA Works
XRDP and GNOME Login Flow With Microsoft Entra ID Credentials and Rublon MFA Diagram

Before You Start
Configure Rublon Authentication Proxy
alice@example.com
Use LDAPS Between Debian and Rublon Authentication Proxy
Protocol: LDAPS
Port: 636
Rublon Authentication Proxy host: rap.example.com
CA certificate: /etc/ssl/certs/rap-internal-ca.pem
Install XRDP and the Required Packages
apt update
apt install -y \
xrdp \
python3-ldap3 \
pamtester
/etc/pam.d/xrdp-sesman
systemctl enable --now xrdp
systemctl status xrdp --no-pager
systemctl status xrdp-sesman --no-pager
Restrict Network Access to XRDP
ufw allow from 10.0.0.0/8 to any port 3389 proto tcp
ufw status numbered
Create or Verify the Linux Account
getent passwd alice
alice:x:1001:1001:Alice:/home/alice:/bin/bash
id alice
ls -ld /home/alice
adduser alice
getent passwd alice
alice
alice@example.com
Prepare an Example PAM Mapping Script
/usr/local/sbin/pam-rap-entra.py
#!/usr/bin/env python3
import os
import pwd
import ssl
import sys
import syslog
from ldap3 import (
AUTO_BIND_NO_TLS,
SIMPLE,
Connection,
Server,
Tls,
)
from ldap3.core.exceptions import LDAPException
# Replace these values with settings for your environment.
RAP_HOST = "rap.example.com"
RAP_PORT = 636
RAP_CA_CERT = "/etc/ssl/certs/rap-internal-ca.pem"
UPN_SUFFIX = "example.com"
CONNECT_TIMEOUT = 10
AUTH_TIMEOUT = 180
MAX_PASSWORD_BYTES = 512
def log_error(message: str) -> None:
syslog.openlog("pam-rap-entra")
syslog.syslog(
syslog.LOG_AUTHPRIV | syslog.LOG_ERR,
message,
)
syslog.closelog()
def log_info(message: str) -> None:
syslog.openlog("pam-rap-entra")
syslog.syslog(
syslog.LOG_AUTHPRIV | syslog.LOG_INFO,
message,
)
syslog.closelog()
def main() -> int:
pam_user = os.environ.get("PAM_USER", "").strip()
pam_service = os.environ.get("PAM_SERVICE", "unknown").strip()
if not pam_user:
log_error("PAM_USER is empty")
return 1
# Debian needs a Linux account resolvable through NSS to provide
# the UID, GID, home directory, shell, file ownership,
# and process ownership.
try:
pwd.getpwnam(pam_user)
except KeyError:
log_error(
f"Local Linux user does not exist: {pam_user}"
)
return 1
# pam_exec.so with expose_authtok passes the authentication
# token to the command through standard input.
raw_password = sys.stdin.buffer.read(MAX_PASSWORD_BYTES)
try:
password = raw_password.rstrip(
b"\x00\r\n"
).decode(
"utf-8",
errors="strict",
)
except UnicodeDecodeError:
log_error(
f"Password decoding failed for user: {pam_user}"
)
return 1
if not password:
log_error(
f"Empty password received for user: {pam_user}"
)
return 1
# Map the local Linux username to the Microsoft Entra ID UPN.
if "@" in pam_user:
entra_upn = pam_user
else:
entra_upn = f"{pam_user}@{UPN_SUFFIX}"
tls_config = Tls(
validate=ssl.CERT_REQUIRED,
ca_certs_file=RAP_CA_CERT,
)
server = Server(
RAP_HOST,
port=RAP_PORT,
use_ssl=True,
tls=tls_config,
connect_timeout=CONNECT_TIMEOUT,
)
connection = None
try:
connection = Connection(
server,
user=entra_upn,
password=password,
authentication=SIMPLE,
auto_bind=AUTO_BIND_NO_TLS,
receive_timeout=AUTH_TIMEOUT,
raise_exceptions=True,
)
log_info(
"Authentication successful: "
f"service={pam_service}, "
f"local={pam_user}, "
f"entra={entra_upn}"
)
return 0
except LDAPException as exc:
log_error(
"Authentication failed: "
f"service={pam_service}, "
f"local={pam_user}, "
f"entra={entra_upn}, "
f"error={exc}"
)
return 1
except Exception as exc:
log_error(
"Unexpected authentication error: "
f"service={pam_service}, "
f"local={pam_user}, "
f"entra={entra_upn}, "
f"error={exc}"
)
return 1
finally:
if connection is not None:
try:
connection.unbind()
except Exception:
pass
if __name__ == "__main__":
sys.exit(main())
Adapt the Example Script
RAP_HOST = "rap.example.com"
UPN_SUFFIX = "example.com"
RAP_CA_CERT = "/etc/ssl/certs/rap-internal-ca.pem"
Secure the Script
chown root:root /usr/local/sbin/pam-rap-entra.py
chmod 0755 /usr/local/sbin/pam-rap-entra.py
python3 -m py_compile /usr/local/sbin/pam-rap-entra.py
Test the Example Helper Before Modifying XRDP or GDM
cat > /etc/pam.d/rap-entra-test <<'EOF'
#%PAM-1.0
auth required pam_exec.so expose_authtok quiet /usr/local/sbin/pam-rap-entra.py
account required pam_permit.so
EOF
pamtester rap-entra-test alice authenticate
pamtester: successfully authenticated
Configure XRDP Authentication
/etc/pam.d/xrdp-sesman
Back Up the XRDP PAM Configuration
cp -a \
/etc/pam.d/xrdp-sesman \
"/root/xrdp-sesman.before-entra-$(date +%F-%H%M%S)"
nl -ba /etc/pam.d/xrdp-sesman
Connect XRDP to the Example Helper
#%PAM-1.0
auth required pam_env.so readenv=1
auth required pam_env.so readenv=1 envfile=/etc/default/locale
# Authenticate with Microsoft Entra ID credentials and Rublon MFA.
auth required pam_exec.so expose_authtok quiet /usr/local/sbin/pam-rap-entra.py
-auth optional pam_gnome_keyring.so
-auth optional pam_kwallet5.so
@include common-account
@include common-password
@include common-session
-session optional pam_gnome_keyring.so auto_start
-session optional pam_kwallet5.so auto_start
cat > /etc/pam.d/xrdp-sesman <<'EOF'
#%PAM-1.0
auth required pam_env.so readenv=1
auth required pam_env.so readenv=1 envfile=/etc/default/locale
# Authenticate with Microsoft Entra ID credentials and Rublon MFA.
auth required pam_exec.so expose_authtok quiet /usr/local/sbin/pam-rap-entra.py
-auth optional pam_gnome_keyring.so
-auth optional pam_kwallet5.so
@include common-account
@include common-password
@include common-session
-session optional pam_gnome_keyring.so auto_start
-session optional pam_kwallet5.so auto_start
EOF
nl -ba /etc/pam.d/xrdp-sesman
systemctl restart xrdp xrdp-sesman
systemctl status xrdp xrdp-sesman --no-pager
Test the XRDP PAM Configuration
pamtester xrdp-sesman alice authenticate
pamtester: successfully authenticated
Connect Through an RDP Client
Protocol: RDP
Server: 192.0.2.10
Username: alice
Password: Microsoft Entra ID password
Domain: Leave empty
alice
Manage an Existing XRDP Session
loginctl list-sessions
pgrep -a -u alice \
'Xorg|gnome-session|gnome-shell|xrdp-chansrv'
loginctl terminate-session SESSION_ID
loginctl kill-session SESSION_ID
Configure GDM and GNOME Screen Unlock
/etc/pam.d/gdm-password
Back Up the GDM PAM Configuration
cp -a \
/etc/pam.d/gdm-password \
"/root/gdm-password.before-entra-$(date +%F-%H%M%S)"
nl -ba /etc/pam.d/gdm-password
Replace the Primary Authentication Entry
@include common-auth
auth required pam_exec.so expose_authtok quiet /usr/local/sbin/pam-rap-entra.py
#%PAM-1.0
auth requisite pam_nologin.so
auth required pam_succeed_if.so user != root quiet_success
# Authenticate with Microsoft Entra ID credentials and Rublon MFA.
auth required pam_exec.so expose_authtok quiet /usr/local/sbin/pam-rap-entra.py
auth optional pam_gnome_keyring.so
@include common-account
nl -ba /etc/pam.d/gdm-password
Test the GDM PAM Configuration
pamtester gdm-password alice authenticate
pamtester: successfully authenticated
Test a Local GDM Login
Test GNOME Screen Unlock
Authentication Behavior After Configuration
/etc/pam.d/xrdp-sesman
/etc/pam.d/gdm-password
/etc/pam.d/common-auth
Logs and Troubleshooting
View Example Helper Logs
journalctl -t pam-rap-entra
journalctl -f -t pam-rap-entra
View XRDP Logs
journalctl -u xrdp -u xrdp-sesman -f
tail -f /var/log/xrdp.log
tail -f /var/log/xrdp-sesman.log
View GDM Logs
journalctl -u gdm3 -f
Test the LDAPS Connection
nc -vz rap.example.com 636
openssl s_client \
-connect rap.example.com:636 \
-servername rap.example.com \
-CAfile /etc/ssl/certs/rap-internal-ca.pem \
</dev/null
Verify return code: 0 (ok)
Repeat the PAM Tests
pamtester rap-entra-test alice authenticate
pamtester xrdp-sesman alice authenticate
pamtester gdm-password alice authenticate
XRDP Rejects the Login Before Rublon MFA Starts
systemctl status xrdp xrdp-sesman --no-pager
journalctl -u xrdp -u xrdp-sesman --since "10 minutes ago"
Rublon MFA Does Not Start
Certificate Validation Fails
XRDP Displays a Previous Locked or Blank Session
loginctl list-sessions
pgrep -a -u alice \
'Xorg|gnome-session|gnome-shell|xrdp-chansrv'
GNOME Cannot Be Unlocked
Restore the Previous Configuration
Restore XRDP Authentication
ls -1t /root/xrdp-sesman.before-entra-* | head
cp -a \
/root/xrdp-sesman.before-entra-YYYY-MM-DD-HHMMSS \
/etc/pam.d/xrdp-sesman
systemctl restart xrdp xrdp-sesman
pamtester xrdp-sesman alice authenticate