NeuroAgent

FreeRADIUS EAP-TTLS/PAP Inner Username Logging Guide

Learn how to 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. Complete guide with configuration examples.

Question

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:

server 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:

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:

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=\"%{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?

NeuroAgent

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

In EAP-TTLS authentication, there are two distinct username flows:

  1. Outer username: This is the identity sent in the clear during the initial handshake. Android clients often send “anonymous” for privacy.

  2. 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:

  • default server handles the outer authentication
  • inner-tunnel server 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:

unlang
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:

unlang
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:

unlang
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:

unlang
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:

unlang
# 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:

unlang
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:

unlang
# 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

  1. FreeRADIUS Documentation - inner-tunnel
  2. FreeRADIUS Wiki - eduroam logging
  3. FreeRADIUS Documentation - linelog module
  4. FreeRADIUS EAP-TTLS configuration
  5. FreeRADIUS EAP configuration guide
  6. FreeRADIUS inner-tunnel username handling
  7. FreeRADIUS EAP-TTLS inner username capture

Conclusion

To successfully log the inner username from EAP-TTLS/PAP authentication in FreeRADIUS, you need to:

  1. Configure the inner-tunnel virtual server to capture and preserve the inner username using update outer.session-state { &User-Name := &User-Name }

  2. Update your linelog configurations to reference the preserved inner username using %{outer.session-state:User-Name} instead of just %{User-Name}

  3. Modify the outer server’s post-auth and accounting sections to properly handle the session state updates

  4. 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.