How can I configure FreeRADIUS with linelog to capture and log the inner username from EAP-TTLS/PAP authentication requests instead of only logging the outer ‘anonymous’ identity?
I’m setting up FreeRADIUS to proxy EAP-TTLS/PAP requests to a RADIUS server that only supports plain PAP authentication. While the proxying and logging features work correctly, the current linelog configuration only logs the User-Name from the outer tunnel request (which is ‘anonymous’ by default for Android clients). I need to access and log the actual username provided inside the inner tunnel for authentication purposes.
Current configuration files:
sites-enabled/default:
listen {
type = auth
ipaddr = *
port = 1812
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
listen {
type = acct
ipaddr = *
port = 1813
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
listen {
type = auth
ipv6addr = ::
port = 1812
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
listen {
type = acct
ipv6addr = ::
port = 1813
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
authorize {
eap {
ok = return
}
pap
update control {
&Proxy-To-Realm := "authentik"
}
}
authenticate {
Auth-Type PAP {
pap
}
eap
}
post-auth {
log_access
update {
&reply: += &session-state:
}
if (&reply:EAP-Session-Id) {
update reply {
EAP-Key-Name := &reply:EAP-Session-Id
}
}
}
post-proxy {
eap
}
accounting {
log_accounting
}
}
mods-enabled/eap:
default_eap_type = ttls
timer_expire = 60
ignore_unknown_eap_types = no
max_sessions = ${max_requests}
tls-config tls-common {
private_key_file = /certs/live/{DOMAIN}/privkey.pem
certificate_file = /certs/live/{DOMAIN}/fullchain.pem
cipher_list = "DEFAULT"
cipher_server_preference = no
tls_min_version = "1.2"
tls_max_version = "1.3"
ecdh_curve = ""
cache {
enable = no
lifetime = 24
store {
Tunnel-Private-Group-Id
}
}
ocsp {
enable = no
override_cert_url = yes
url = "http://127.0.0.1/ocsp/"
}
}
ttls {
tls = tls-common
default_eap_type = pap
copy_request_to_tunnel = no
use_tunneled_reply = no
virtual_server = "default"
}
}
mods-enabled/linelog:
filename = ${logdir}/custom/log_access.log
permissions = 0600
reference = "messages.%{%{reply:Packet-Type}:-default}"
messages {
default = "timestamp=\"%t\" event=\"ACCESS-OTHER\" packet_type=\"%{Packet-Type}\" user=\"%{User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
Access-Accept = "timestamp=\"%t\" event=\"ACCESS-ACCEPT\" user=\"%{User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
Access-Reject = "timestamp=\"%t\" event=\"ACCESS-REJECT\" user=\"%{User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" reason=\"%{Module-Failure-Message}\""
}
}
linelog log_accounting {
filename = ${logdir}/custom/log_accounting.log
permissions = 0600
reference = "messages.%{%{Acct-Status-Type}:-default}"
messages {
default = "timestamp=\"%t\" event=\"ACCT-OTHER\" acct_status=\"%{Acct-Status-Type}\" user=\"%{User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" session_id=\"%{Acct-Session-Id}\""
Start = "timestamp=\"%t\" event=\"ACCT-START\" user=\"%{User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" session_id=\"%{Acct-Session-Id}\""
Stop = "timestamp=\"%t\" event=\"ACCT-STOP\" user=\"%{User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" session_id=\"%{Acct-Session-Id}\" duration_sec=\"%{Acct-Session-Time}\""
Accounting-On = "timestamp=\"%t\" event=\"ACCT-ON\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
Accounting-Off = "timestamp=\"%t\" event=\"ACCT-OFF\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
}
}
Current logs:
log_access.log:
timestamp="Wed Nov 5 17:30:31 2025" event="ACCESS-ACCEPT" user="someuser" client_mac="" nas_id="" nas_ip=""
timestamp="Wed Nov 5 17:30:29 2025" event="ACCESS-ACCEPT" user="anonymous" client_mac="someid" nas_id="someid" nas_ip="10.0.5.x"
log_accounting.log:
timestamp="Wed Nov 5 17:29:15 2025" event="ACCT-STOP" user="anonymous" client_mac="someid" nas_id="someid" nas_ip="10.0.5.x" session_id="4DCA6606E70B1D31" duration_sec="3333"
timestamp="Wed Nov 5 17:30:34 2025" event="ACCT-START" user="anonymous" client_mac="someid" nas_id="someid" nas_ip="10.0.5.x" session_id="02A4FDCAB6135746"
What specific changes do I need to make to my FreeRADIUS configuration to capture and log the inner username in both access and accounting logs?
To capture and log the inner username from EAP-TTLS/PAP authentication requests, you need to modify your FreeRADIUS configuration to preserve the inner username during the authentication process and reference it properly in your linelog configuration.
The core issue is that your current configuration only references the outer User-Name attribute, which appears as “anonymous” for Android clients. You need to access the actual username provided in the inner tunnel.
Contents
- Understanding Inner/Outer Username Flow
- Required Configuration Changes
- Modifying the inner-tunnel Virtual Server
- Updating linelog Module Configuration
- Testing and Verification
- Alternative Approaches
Understanding Inner/Outer Username Flow
In EAP-TTLS authentication, there are two distinct username flows:
-
Outer username: This is the identity sent in the clear during the initial handshake. Android clients often send “anonymous” for privacy.
-
Inner username: This is the actual username sent within the encrypted TTLS tunnel during the inner authentication process (PAP in your case).
FreeRADIUS processes these through two separate virtual servers:
defaultserver handles the outer authenticationinner-tunnelserver handles the inner authentication
The inner username is available in the inner-tunnel virtual server but needs to be preserved and transferred back to the outer session for logging.
Required Configuration Changes
Modifying the inner-tunnel Virtual Server
First, you need to ensure your inner-tunnel virtual server is properly configured to capture and preserve the inner username. Create or edit /etc/raddb/sites-enabled/inner-tunnel:
server inner-tunnel {
authorize {
# Your existing authorize modules here
# Make sure PAP authentication is enabled
pap
}
authenticate {
Auth-Type PAP {
pap
}
}
post-auth {
# Update the session-state to preserve the inner username
# This makes the inner username available in the outer session
update outer.session-state {
&User-Name := &User-Name
}
# Optional: Log the inner authentication separately
linelog inner_auth {
filename = ${logdir}/custom/inner_auth.log
reference = "messages.%{%{reply:Packet-Type}:-default}"
messages {
default = "timestamp=\"%t\" event=\"INNER-OTHER\" inner_user=\"%{User-Name}\" outer_user=\"%{outer.request:User-Name}\""
Access-Accept = "timestamp=\"%t\" event=\"INNER-ACCEPT\" inner_user=\"%{User-Name}\" outer_user=\"%{outer.request:User-Name}\""
Access-Reject = "timestamp=\"%t\" event=\"INNER-REJECT\" inner_user=\"%{User-Name}\" outer_user=\"%{outer.request:User-Name}\" reason=\"%{Module-Failure-Message}\""
}
}
# Continue with existing post-auth modules
}
}
Updating linelog Module Configuration
Now modify your log_access linelog configuration to reference the preserved inner username:
linelog log_access {
filename = ${logdir}/custom/log_access.log
permissions = 0600
reference = "messages.%{%{reply:Packet-Type}:-default}"
messages {
default = "timestamp=\"%t\" event=\"ACCESS-OTHER\" packet_type=\"%{Packet-Type}\" user=\"%{outer.session-state:User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
Access-Accept = "timestamp=\"%t\" event=\"ACCESS-ACCEPT\" user=\"%{outer.session-state:User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
Access-Reject = "timestamp=\"%t\" event=\"ACCESS-REJECT\" user=\"%{outer.session-state:User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" reason=\"%{Module-Failure-Message}\""
}
}
Similarly, update your log_accounting configuration:
linelog log_accounting {
filename = ${logdir}/custom/log_accounting.log
permissions = 0600
reference = "messages.%{%{Acct-Status-Type}:-default}"
messages {
default = "timestamp=\"%t\" event=\"ACCT-OTHER\" acct_status=\"%{Acct-Status-Type}\" user=\"%{outer.session-state:User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" session_id=\"%{Acct-Session-Id}\""
Start = "timestamp=\"%t\" event=\"ACCT-START\" user=\"%{outer.session-state:User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" session_id=\"%{Acct-Session-Id}\""
Stop = "timestamp=\"%t\" event=\"ACCT-STOP\" user=\"%{outer.session-state:User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" session_id=\"%{Acct-Session-Id}\" duration_sec=\"%{Acct-Session-Time}\""
Accounting-On = "timestamp=\"%t\" event=\"ACCT-ON\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
Accounting-Off = "timestamp=\"%t\" event=\"ACCT-OFF\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
}
}
Modifying the outer server configuration
Update your default server to properly handle the session state updates:
server default {
# ... existing listen configurations ...
authorize {
eap {
ok = return
}
pap
update control {
&Proxy-To-Realm := "authentik"
}
}
authenticate {
Auth-Type PAP {
pap
}
eap
}
post-auth {
# Update session-state to include inner username
update session-state {
&Tmp-String-1 := "accept"
}
# This will copy the inner username to the outer session
if (&reply:EAP-Session-Id) {
update reply {
EAP-Key-Name := &reply:EAP-Session-Id
}
}
log_access
# This ensures the inner username is available for the final response
update {
&reply: += &session-state:
}
}
post-proxy {
eap
}
accounting {
# Update accounting to use inner username
update request {
User-Name := "%{outer.session-state:User-Name}"
}
log_accounting
}
}
Testing and Verification
After making these changes, restart FreeRADIUS and test the authentication. Your logs should now show the inner username instead of “anonymous”:
Expected log output:
# log_access.log
timestamp="Wed Nov 5 17:30:31 2025" event="ACCESS-ACCEPT" user="actual_user" client_mac="someid" nas_id="someid" nas_ip="10.0.5.x"
timestamp="Wed Nov 5 17:30:29 2025" event="ACCESS-ACCEPT" user="actual_user" client_mac="someid" nas_id="someid" nas_ip="10.0.5.x"
# log_accounting.log
timestamp="Wed Nov 5 17:29:15 2025" event="ACCT-STOP" user="actual_user" client_mac="someid" nas_id="someid" nas_ip="10.0.5.x" session_id="4DCA6606E70B1D31" duration_sec="3333"
timestamp="Wed Nov 5 17:30:34 2025" event="ACCT-START" user="actual_user" client_mac="someid" nas_id="someid" nas_ip="10.0.5.x" session_id="02A4FDCAB6135746"
# Optional inner_auth.log
timestamp="Wed Nov 5 17:30:31 2025" event="INNER-ACCEPT" inner_user="actual_user" outer_user="anonymous"
Alternative Approaches
If the above approach doesn’t work, you can try these alternatives:
Method 1: Using User-Name with nostrip
Ensure your realm configuration includes the nostrip option to preserve the full username:
# In realms or sites-available/default
realm example.com {
authhost = localhost
accthost = localhost
nostrip = yes
}
Method 2: Using Inner-Tunnel-User-Name
Some FreeRADIUS versions support the Inner-Tunnel-User-Name attribute:
linelog log_access {
# ... existing configuration ...
messages {
Access-Accept = "timestamp=\"%t\" event=\"ACCESS-ACCEPT\" user=\"%{Inner-Tunnel-User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
Access-Reject = "timestamp=\"%t\" event=\"ACCESS-REJECT\" user=\"%{Inner-Tunnel-User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\" reason=\"%{Module-Failure-Message}\""
}
}
Method 3: Using EAP-Message parsing
As a fallback, you can parse the EAP-Message to extract the inner username:
# In post-auth section
update control {
&Inner-User-Name := "%{mschap:User-Name}"
}
linelog log_access {
messages {
Access-Accept = "timestamp=\"%t\" event=\"ACCESS-ACCEPT\" user=\"%{control:Inner-User-Name}\" client_mac=\"%{Calling-Station-Id}\" nas_id=\"%{NAS-Identifier}\" nas_ip=\"%{NAS-IP-Address}\""
}
}
Remember to test thoroughly after each configuration change, and monitor your FreeRADIUS logs for any errors or authentication failures. The key is ensuring that the inner username is preserved through the authentication chain and properly referenced in your logging configurations.
Sources
- FreeRADIUS Documentation - inner-tunnel
- FreeRADIUS Wiki - eduroam logging
- FreeRADIUS Documentation - linelog module
- FreeRADIUS EAP-TTLS configuration
- FreeRADIUS EAP configuration guide
- FreeRADIUS inner-tunnel username handling
- FreeRADIUS EAP-TTLS inner username capture
Conclusion
To successfully log the inner username from EAP-TTLS/PAP authentication in FreeRADIUS, you need to:
-
Configure the inner-tunnel virtual server to capture and preserve the inner username using
update outer.session-state { &User-Name := &User-Name } -
Update your linelog configurations to reference the preserved inner username using
%{outer.session-state:User-Name}instead of just%{User-Name} -
Modify the outer server’s post-auth and accounting sections to properly handle the session state updates
-
Test thoroughly to ensure the username flow works correctly and appears in your logs
The key insight is that FreeRADIUS maintains separate attribute spaces for inner and outer authentication, and you need explicitly copy the inner username to the outer session state for logging purposes. This approach maintains security while providing the detailed authentication logs you need for monitoring and troubleshooting.