Project

General

Profile

Actions

Feature #16582

open

FreeRADIUS: Add configurable TOTP anti-replay protection (RFC 6238)

Added by Loic FONTAINE 3 days ago. Updated 3 days ago.

Status:
New
Priority:
Normal
Assignee:
-
Category:
FreeRADIUS
Target version:
-
Start date:
Due date:
% Done:

0%

Estimated time:
Plus Target Version:

Description

  1. Feature Request: TOTP Anti-Replay Protection with GUI Option for FreeRADIUS
  1. Summary

Add configurable anti-replay protection to the Google Authenticator OTP implementation in the FreeRADIUS3 package. This includes:
1. A new checkbox in Services > FreeRADIUS > Settings to enable/disable anti-replay
2. Modified `googleauth.py` script that respects this setting
3. Full backward compatibility (disabled by default)

  1. Problem Statement

The current `googleauth.py` implementation accepts TOTP codes within a ±30 second window but does not track whether a code has already been used. This allows replay attacks where an attacker who intercepts a valid code can reuse it within the validity window.

RFC 6238 Section 5.2 states:

"The verifier MUST NOT accept the second attempt of the OTP after the successful validation has been issued for the first OTP, which ensures one-time only use of an OTP."

  1. Current Behavior
    ```
    User authenticates with PIN 1234 + OTP 567890 → SUCCESS
    Same credentials replayed within 30 seconds → SUCCESS (VULNERABILITY)
    ```
  1. Expected Behavior (when enabled)
    ```
    User authenticates with PIN 1234 + OTP 567890 → SUCCESS
    Same credentials replayed within 30 seconds → REJECT (token already used)
    ```
  1. Proposed Solution
  1. 1. GUI Modification (freeradiussettings.xml)

Add a new checkbox field in the OTP settings section:

```xml
<field>
<fielddescr>Enable OTP Anti-Replay Protection</fielddescr>
<fieldname>varotpantireplay</fieldname>
<description>
<![CDATA[
Enable anti-replay protection for OTP authentication.<br/>
<span class="text-info">When enabled, each OTP code can only be used once
within its validity window (90 seconds).</span><br/>
<span class="text-warning">Provides RFC 6238 compliance and prevents
replay attacks.</span><br/>
<span class="text-info">(Default: unchecked)</span>
]]>
</description>
<type>checkbox</type>
<default_value></default_value>
</field>
```

  1. 2. Backend Modification (freeradius.inc)

Pass the setting as a 5th parameter to the googleauth.py script:

Current:
```php
program = "{$varFREERADIUS_SCRIPTS}/googleauth.py %{request:User-Name} %{reply:MOTP-Init-Secret} %{reply:MOTP-PIN} %{request:User-Password}"
```

Modified:
```php
$varotpantireplay = ($settingsconfig['varotpantireplay'] == 'on') ? '1' : '0';
// ...
program = "{$varFREERADIUS_SCRIPTS}/googleauth.py %{request:User-Name} %{reply:MOTP-Init-Secret} %{reply:MOTP-PIN} %{request:User-Password} {$varotpantireplay}"
```

  1. 3. Script Modification (googleauth.py)

Modified script that:
- Accepts optional 5th parameter for anti-replay toggle
- Maintains full backward compatibility (disabled if parameter missing)
- Uses file-based token cache in `/var/run/freeradius/`
- Implements automatic cleanup of expired entries

Key additions:
```python
def is_token_used(username, token_code):
"""Check if token was already used.""" # Returns True if found in cache and not expired

def mark_token_used(username, token_code):
"""Record token with timestamp.""" # Atomic write with file locking

def authenticate(username, secretkey, pin, password, antireplay_enabled=False): # ... existing validation ...
if antireplay_enabled and is_token_used(username, otp_attempt):
syslog.syslog(syslog.LOG_WARNING, "REPLAY ATTACK DETECTED...")
return False # ... TOTP validation ...
if valid and antireplay_enabled:
mark_token_used(username, otp_attempt)
return valid
```

  1. Security Impact

- Risk Level: Medium (elevated to High in compliance-sensitive environments)
- Attack Vector: Man-in-the-middle interception
- Mitigation: TLS encryption in OpenVPN reduces but doesn't eliminate risk
- Compliance: Required for PCI-DSS, ISO 27001, SOC2 environments

  1. Competitor Comparison
Solution Anti-Replay GUI Option
---------- ------------- ------------
pfSense FreeRADIUS (current) N/A
pfSense FreeRADIUS (proposed) *✅* *✅*
OPNsense FreeRADIUS N/A
FortiAuthenticator
PrivacyIDEA
Google Authenticator PAM Config file
  1. Backward Compatibility

Full backward compatibility guaranteed:
- Default: disabled (unchecked)
- Existing installations unaffected
- Script accepts missing 5th parameter gracefully
- No configuration migration required

  1. Testing Procedure
```bash
  1. 1. Enable anti-replay in GUI
  2. Services > FreeRADIUS > Settings > Enable OTP Anti-Replay Protection
  1. 2. Restart FreeRADIUS
    /usr/local/etc/rc.d/radiusd restart
  1. 3. Test first authentication (should succeed)
    otp=$(oathtool --totp -b $SECRET)
    radtest user ${PIN}${otp} 127.0.0.1 1812 $SHARED_SECRET
  2. Expected: Access-Accept
  1. 4. Test replay (should fail)
    radtest user ${PIN}${otp} 127.0.0.1 1812 $SHARED_SECRET
  2. Expected: Access-Reject
  1. 5. Verify log message
    grep "REPLAY ATTACK DETECTED" /var/log/messages
    ```
  1. Files Modified
File Change
------ --------
`/usr/local/pkg/freeradiussettings.xml` Add checkbox field
`/usr/local/pkg/freeradius.inc` Pass parameter to script
`/usr/local/etc/raddb/scripts/googleauth.py` Implement anti-replay logic
  1. Implementation Notes
  1. Token Storage
    - Location: `/var/run/freeradius/totp_used_tokens`
    - Format: `username:token:timestamp` (one per line)
    - Cleared on reboot (RAM-backed)
    - Auto-cleanup of expired entries
  1. Performance
    - Negligible impact (~1ms per authentication)
    - File size self-limiting via cleanup
    - File locking prevents race conditions
  1. Edge Cases Handled
    - Missing parameter: defaults to disabled
    - Missing cache directory: auto-created
    - File I/O errors: fail-open (authentication proceeds)
    - Concurrent authentications: file locking
  1. References

- [RFC 6238 - TOTP Algorithm](https://datatracker.ietf.org/doc/html/rfc6238) - Section 5.2
- [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) - Digital Identity Guidelines
- [FortiAuthenticator Anti-Replay](https://docs.fortinet.com/document/fortiauthenticator/6.4.0/administration-guide/996353)

  1. Attachments

Complete implementation files are attached:
1. `patch-freeradiussettings.xml` - GUI field definition
2. `patch-freeradius.inc.php` - PHP backend modification guide
3. `googleauth-configurable.py` - Complete modified script


Environment:
- pfSense version: 2.7.x / 2.8.x / 24.x
- Package: freeradius3
- Tested on: pfSense 2.8.1

Submitter:
This enhancement addresses a known security gap while maintaining full backward compatibility. The GUI toggle allows administrators to enable RFC 6238 compliance when required by their security policies without affecting existing deployments.


Files

googleauth-configurable.py (6.84 KB) googleauth-configurable.py Loic FONTAINE, 12/13/2025 07:49 AM
patch-freeradius-inc-detailed.diff (3.32 KB) patch-freeradius-inc-detailed.diff Loic FONTAINE, 12/13/2025 07:49 AM
patch-freeradiussettings-xml-detailed.txt (3.01 KB) patch-freeradiussettings-xml-detailed.txt Loic FONTAINE, 12/13/2025 07:49 AM
freeradiussettings.xml.patch (1018 Bytes) freeradiussettings.xml.patch Loic FONTAINE, 12/13/2025 08:00 AM
freeradius.inc.patch (1005 Bytes) freeradius.inc.patch Loic FONTAINE, 12/13/2025 08:02 AM
Actions

Also available in: Atom PDF