Table of Contents

Radius VoIP Calls Application

Purpose & Scope

High Level Architecture

[SBC] —- [Cisco CUBE (Radius)] —- [RHEL - Radius Server + Radius REST Module)] —- [RHE Linux - Web Server (Custom API)] — [SQL Server Database (Single Table)]

Sample Cisco CUBE Config

Note - by default CUBE would only failover to the second Radius server, if and when the requessts to the first one timed out. However this command radius-server dead-criteria time 5 tries 3 means if that we have 3 failed attempts in a row and we have NOT had a valid reply in the last 5 seconds, then mark the server as DEAD!

We then have a deadtimer at the server level, in the below case of 60 mins (max if 1440 mins (24 hours)) - so when tehre is a failover - it will stay witj the backup server for an hour and THEN failback. If the server is still down, it will get marked as Dead again until the next hour etc.

!
radius-server dead-criteria time 5 tries 3
!
aaa new-model
aaa group server radius FREERADIUS
 deadtime 60 ! 60 minutes
 server name RAD1
 server name RAD2
!
aaa accounting update newinfo ! *** the config enables 'Interim-Update' accounting records - such as when call is connected, instead of just start and end records. *** 
!
aaa accounting connection CUBE-CDR start-stop group FREERADIUS
!
radius server RAD1
 address ipv4 10.123.123.101
  key MySuperSecret ! *** This key (and the below one) - is used in the Radius client setup ***
 timeout 2
 retransmit 1  
!
radius server RAD2
 address ipv4 10.123.123.102
  key MySuperSecret 
 timeout 2
 retransmit 1  
!
 gw-accounting aaa
 method CUBE-CDR
!
! This will be applied to dial-peers which are NOT to send accounting records
voice class aaa 1000
 accounting suppress 
!
ip radius source-interface Loopback0
!
dial-peer 100 voip
 description This is the dial-peer used to send and receive calls to the PSTN Provider
 ! *** NOTE - we do NOT configure voice-class aaa 1000 on this dial-peer - otherwise we would NOT get sent a Radius accounting record
 session transport udp
 incoming called e164-pattern-map 100
 destination dpg 100
 voice-class codec 100 offer-all
 dtmf-relay rtp-nte
 no vad
!
dial-peer 200 voip
 description This is an example dial-peer to send calls to CUCM 
! *** NOTE - we DO configure voice-class aaa 1000 on this dial-peer - as we do NOT want a Radius Record for calls active on this dial-peer (otherwise we will get duplicate records and not know if the call is a "incoming" or "outgoing" call)
 incoming called e164-pattern-map 200
 destination dpg 200
 voice-class aaa 1000 ! *** This line supresses Radius records for this dialpeer ***
 incoming called 3164-pattern-map 131
 voice-class codec 100 offer-all
 dtmf-relay rtp-nte
 no vad
!

Redhat Enterprise Linux (RHEL)

Useful Commands

Checking Repo list

dnf repolist 

Check config (before restarting)

sudo radiusd -XC

Status, Start, Stop, Restart FreeRadius

systemctl status radiusd
systemctl start radiusd
systemctl stop radiusd
systemctl restart radiusd

Which Services are set to auto startup

systemctl list-unit-files --type=service --state=enabled

Which Services are set running

systemctl list-units --type=service --state=running

Check Firewall ports

sudo firewall-cmd --list-all

List all the Repositorities, their IDs, their names and if they are enabled

sudo dnf repolist --all

Just lists the Enabled Repositorities Names

sudo dnf repolist --enabled

Installing & Configuration of Radius

Initial freeRadius Setup (Basic config - step 1)

  1. Install freeradius
    1. sudo yum install freeradius
  2. Enable Service
    1. sudo systemctl enable radiusd
  3. Remove default config symlinks (this locks the radius config down, by removing the default config which will be replaced with a very specific config file 'cube-accounting')
    1. sudo rm /etc/raddb/sites-enabled/inner-tunnel
    2. sudo rm /etc/raddb/sites-enabled/default
  1. Add custom config file cube-accounting - i.e. use VI and copy and paste in the config and save the file
    1. note this sample config does NOT contain any REST request - but this will get it accounting records working
    2. sudo vi /etc/raddb/sites-available/cube-accounting
    3. server cube-accounting {
              listen {
                      type = acct
                      ipaddr = *
                      port = 1813
                      proto = udp
              }
       
              recv Accounting-Request {
                      accounting
              }
       
              accounting {
                      detail
              }
      }
  2. Fix ownership of the above config file:
    1. sudo chown root:radiusd /etc/raddb/sites-available/cube-accounting
  3. add symlink for config file
    1. sudo ln -s /etc/raddb/sites-available/cube-accounting /etc/raddb/sites-enabled/cube-accounting
  4. Fix ownership of the symlink - note the -h
    1. sudo chown -h root:radiusd /etc/raddb/sites-enabled/cube-accounting
  5. Remove the EAP module (as we won't be using it - and it will causes an config check error
    1.  sudo rm /etc/raddb/mods-enabled/eap 
  6. Then check config is good
    1. sudo radiusd -XC
  7. And start freeradius (and then check its status)
    1. sudo systemctl start radiusd
    2. sudo systemctl status radiusd
  8. Open Radius Accounting port - UDP 1813
    1. sudo firewall-cmd --permanent --add-port=1813/udp
  9. Reload Firewall to make config active
    1. sudo firewall-cmd --reload
  10. Edit the /etc/raddb/clients.conf file, comment out all the localhost config, add your cube config (sample below), Save and check config using the sudo radiusd -XC command and then restart the radiusd service
  1. Comment out the client localhost AND client localhost_ipv6 sections
  2. sudo vi /etc/raddb/clients.conf
  3. client cisco-cube-test {
           ipaddr          = 10.123.123.0/24
           secret          = MySuperSecret!
    }

Accounting files will be stored here:

sudo ls -al /var/log/radius/radacct

Radius's - Advanced Setup (Step2) - REST Module

Install the Rest Module

sudo dnf install freeradius-rest

Configure Rest Module

Lets update the simple site config we have above to now call the rest module for start and stop events:

sudo vi /etc/raddb/sites-available/cube-accounting
server cube-accounting {
        listen {
                type = acct
                ipaddr = *
                port = 1813
                proto = udp
        }
 
        recv Accounting-Request {
                accounting
        }
 
        accounting {
                # Log Records to detail text file - useful for troubleshooting and contains other info - taht we are not sening to teh database (GOR)
                detail
                #
                # Voice START records only
                #
 
                # Voice Interim-Update records only - which also have the 'h323-connect-time' attribute.
                if (&Acct-Status-Type == Interim-Update) {
 
                        if (&h323-connect-time){
 
                                if (&h323-setup-time && &h323-setup-time =~ /^h323-setup-time=(.*)$/) {
                                        update request {
                                                &Tmp-String-0 := "%{1}"
                                        }
                                }
 
                                if (&h323-gw-id && &h323-gw-id =~ /^h323-gw-id=(.*)$/) {
                                   update request {
                                           &Tmp-String-1 := "%{1}"
                                   }
                                }
 
                                if (&h323-conf-id && &h323-conf-id =~ /^h323-conf-id=(.*)$/) {
                                        update request {
                                                &Tmp-String-2 := "%{1}"
                                        }
                                }
 
                                if (&h323-call-origin && &h323-call-origin =~ /^h323-call-origin=(.*)$/) {
                                        update request {
                                                &Tmp-String-3 := "%{1}"
                                        }
                                }
                                if (&h323-connect-time =~ /^h323-connect-time=(.*)$/) {
                                        update request {
                                                &Tmp-String-4 := "%{1}"
                                        }
                                }
 
                                foreach &Cisco-AVPair {
                                        if ("%{Foreach-Variable-0}" =~ /^gw-rxd-cdn=ton:0,npi:0,#:(.+)$/) {
                                                update request {
                                                        Tmp-String-5 := "%{1}"
                                                }
                                                break
                                        }
                                }
 
                                rest_voice_connect
                        }
                }
 
                #
                # Voice STOP records only
                #
                elsif (&Acct-Status-Type == Stop) {
 
                        if (&h323-conf-id && &h323-conf-id =~ /^h323-conf-id=(.*)$/) {
                                update request {
                                                &Tmp-String-0 := "%{1}"
                                }
                        }
 
                       if (&h323-connect-time && &h323-connect-time =~ /^h323-connect-time=(.*)$/) {
                                update request {
                                                &Tmp-String-1 := "%{1}"
                                }
                        }
 
                       if (&h323-disconnect-time && &h323-disconnect-time =~ /^h323-disconnect-time=(.*)$/) {
                                update request {
                                                &Tmp-String-2 := "%{1}"
                                }
                        }
 
                        if (&h323-disconnect-cause && &h323-disconnect-cause =~ /^h323-disconnect-cause=(.*)$/) {
                                update request {
                                                &Tmp-String-3 := "%{1}"
                               }
                        }
 
                        if (&h323-remote-address && &h323-remote-address =~ /^h323-remote-address=(.*)$/) {
                                update request {
                                               &Tmp-String-4 := "%{1}"
                                }
                        }
 
                        rest_voice_stop
                }
 
                #
                # Interim-Update and other accounting types:
                # no REST request - GOR reminder - check out the following Cisco Command: aaa accounting update newinfo
                #
        }
}

We will send a Rest request on start and end events. Lets create and configure two rest config files.

sudo vi /etc/raddb/mods-available/rest_voice_connect
rest rest_voice_connect {
    connect_uri = "http://127.0.0.1"
 
    accounting {
        uri = "${..connect_uri}/api/v1/radius-voice-calls.php"
        method = "put"
        body = "json"
        data = '{"Acct-Session-Id": "%{jsonquote:%{Acct-Session-Id}}","h323-setup-time": "%{jsonquote:%{Tmp-String-0}}","h323-gw-id": "%{jsonquote:%{Tmp-String-1}}","h323-conf-id": "%{jsonquote:%{Tmp-String-2}}","h323-call-origin": "%{jsonquote:%{Tmp-String-3}}","h323-connect-time": "%{jsonquote:%{Tmp-String-4}}","gw-cdn": "%{jsonquote:%{Tmp-String-5}}","Called-Station-Id": "%{jsonquote:%{Called-Station-Id}}","User-Name": "%{jsonquote:%{User-Name}}","NAS-IP-Address": "%{jsonquote:%{NAS-IP-Address}}","Acct-Status-Type": "Interim-Update"}'
    }
}
sudo vi /etc/raddb/mods-available/rest_voice_stop
rest rest_voice_stop {
    connect_uri = "http://127.0.0.1"
 
    accounting {
        uri = "${..connect_uri}/api/v1/radius-voice-calls.php"
        method = "put"
        body = "json"
        data = '{"Acct-Session-Id": "%{jsonquote:%{Acct-Session-Id}}","h323-conf-id": "%{jsonquote:%{Tmp-String-0}}","h323-connect-time": "%{jsonquote:%{Tmp-String-1}}","h323-disconnect-time": "%{jsonquote:%{Tmp-String-2}}","h323-disconnect-cause": "%{jsonquote:%{Tmp-String-3}}","h323-remote-address": "%{jsonquote:%{Tmp-String-4}}","Acct-Session-Time": "%{Acct-Session-Time}","User-Name": "%{jsonquote:%{User-Name}}" ,"Acct-Status-Type": "Stop"}'
    }
}

Create Symlinks and update the config files and links ownership.

sudo ln -s ../mods-available/rest_voice_connect /etc/raddb/mods-enabled/rest_voice_connect
sudo ln -s ../mods-available/rest_voice_stop /etc/raddb/mods-enabled/rest_voice_stop
sudo chown root:radiusd /etc/raddb/mods-available/rest_voice_connect
sudo chown root:radiusd /etc/raddb/mods-available/rest_voice_stop
sudo chown -h root:radiusd /etc/raddb/mods-enabled/rest_voice_connect
sudo chown -h root:radiusd /etc/raddb/mods-enabled/rest_voice_stop

Radius Accounting Example Records

Acct-Status-Type

We will have a Start, Interim-Update and Stop records. Start Records occur at call setup, Interim-Update on connect and Stop when call disconnected.

h323-gw-id

The hostname of the CUBE

h323-conf-id

This is the Cisco GUID and is also available for inbound calls to the Finesse Desktop via the ECC variable user.media.id

h323-call-origin

This lets us know if the radius record is coming from an inbound or outbound dialpeer.

Called-Station-Id

The Called Party Number

User-Name

The Calling Party Number

NAS-IP-Address

the IP address of the Cisco CUBE - and since our Radius config specifies to use the Loopback 0 this will be the IP address of Loopback 0 of the CUBE.

Timestamp

The Time in epoch time (to the second) - not quite as accurate at the h323-setup-time - which is in milliseconds.

release-source

Note - only available on 'Stop' Records

This allow us to know if the Calling or Called Party disconnected the call

ValueCisco's definitionInterpretation
1Calling party located in PSTNCalling party on the PSTN side released the call
2Calling party located in VoIP networkCalling party on the VoIP side released the call
3Called party located in PSTNCalled party on the PSTN side released the call
4Called party located in VoIP networkCalled party on the VoIP side released the call

Since we are using a CUBE - we will only ever have values of '2' or '4'

Other key fields

Note - only available on 'Stop' Records

Call Example

Note - that the STOP record has a lot more info, including Connect Time, which party disconnected the call and VoIP Quality info.

Wed Oct 14 11:01:22 2026
        Acct-Session-Id = "000057D6"
        h323-setup-time = "h323-setup-time=11:01:22.259 IRL Wed Oct 14 2026"
        h323-gw-id = "h323-gw-id=CUBE-A.example.org"
        h323-conf-id = "h323-conf-id=0B31D45A C6ED11F1 8491B15D B7BDE126"
        h323-call-origin = "h323-call-origin=answer"
        h323-call-type = "h323-call-type=VoIP"
        Cisco-AVPair = "h323-incoming-conf-id=0B31D45A C6ED11F1 8491B15D B7BDE126"
        Cisco-AVPair = "feature-vsa=fn:TWC,ft:10/14/2026 11:01:22.259,cgn:+35312399138,cdn:+35312612800,frs:0,fid:179819,fcid:B31D45AC6ED11F18491B15DB7BDE126,legID:287C402"
        Called-Station-Id = "+35312612800"
        Cisco-AVPair = "call-id=SDr82a401-1a877a5a8a67402e82fd3232747012bb-a04bk71030"
        Cisco-AVPair = "iphop=count:1"
        Cisco-AVPair = "iphop=hop1:10.123.123.36"
        Cisco-AVPair = "subscriber=Unknown"
        Cisco-AVPair = "session-protocol=sipv2"
        Cisco-AVPair = "gw-rxd-cdn=ton:0,npi:0,#:+35312612800"
        User-Name = "+35312399138"
        Cisco-AVPair = "connect-progress=Call Up"
        Acct-Authentic = Local
        Acct-Status-Type = Start
        Service-Type = Login-User
        NAS-IP-Address = 10.123.123.22
        Acct-Delay-Time = 0
        Timestamp = 1791972082

Wed Oct 14 11:01:45 2026
        Acct-Session-Id = "000057D6"
        h323-setup-time = "h323-setup-time=11:01:22.259 IRL Wed Oct 14 2026"
        h323-gw-id = "h323-gw-id=CUBE-A.example.org"
        h323-conf-id = "h323-conf-id=0B31D45A C6ED11F1 8491B15D B7BDE126"
        h323-call-origin = "h323-call-origin=answer"
        h323-call-type = "h323-call-type=VoIP"
        Cisco-AVPair = "h323-incoming-conf-id=0B31D45A C6ED11F1 8491B15D B7BDE126"
        Cisco-AVPair = "feature-vsa=fn:TWC,ft:10/14/2026 11:01:22.259,cgn:+35312399138,cdn:+35312612800,frs:0,fid:179819,fcid:B31D45AC6ED11F18491B15DB7BDE126,legID:287C402"
        Called-Station-Id = "12800"
        Cisco-AVPair = "call-id=SDr82a401-1a877a5a8a67402e82fd3232747012bb-a04bk71030"
        Cisco-AVPair = "iphop=count:1"
        Cisco-AVPair = "iphop=hop1:10.123.123.36"
        Cisco-AVPair = "subscriber=Unknown"
        Cisco-AVPair = "session-protocol=sipv2"
        Cisco-AVPair = "gw-rxd-cdn=ton:0,npi:0,#:+35312612800"
        h323-connect-time = "h323-connect-time=11:01:22.317 IRL Wed Oct 14 2026"
        h323-disconnect-time = "h323-disconnect-time=11:01:45.222 IRL Wed Oct 14 2026"
        h323-disconnect-cause = "h323-disconnect-cause=10"
        h323-remote-address = "h323-remote-address=10.123.123.36"
        Cisco-AVPair = "gw-rxd-cgn=ton:0,npi:0,pi:0,si:0,#:+35312399138"
        Acct-Input-Octets = 222304
        Acct-Output-Octets = 220800
        Acct-Input-Packets = 1124
        Acct-Output-Packets = 1104
        Cisco-AVPair = "MOS-Con=0.0000"
        Cisco-AVPair = "round-trip-delay=0 ms"
        Cisco-AVPair = "receive-delay=0 ms"
        Cisco-AVPair = "voice-quality-total-packet-loss=0.00 %"
        Cisco-AVPair = "voice-quality-out-of-order=0.00 %"
        Acct-Session-Time = 23
        Cisco-AVPair = "release-source=2"
        h323-voice-quality = "h323-voice-quality=0"
        Cisco-AVPair = "remote-media-address=10.123.123.36"
        User-Name = "+35312399138"
        Acct-Authentic = Local
        Acct-Status-Type = Stop
        Service-Type = Login-User
        NAS-IP-Address = 10.123.123.22
        Acct-Delay-Time = 0
        Timestamp = 1791972105

Radius Detail log Purging

FreeRadius does NOT use logrotate to rotate the logs. It has configured in the Radius config file /etc/raddb/mods-enabled/detail the following line:

filename = ${radacctdir}/%{%{Packet-Src-IP-Address}:-%{Packet-Src-IPv6-Address}}/detail-%Y%m%d

This means that freeRadius itself is rotating the log file every day.

So the simpliest solution to radius detail files files older 30 days is the following:

Useful commands to view file counts and size used

Counts number of detail files

sudo find /var/log/radius/radacct -type f -name "detail-*" | wc -l

Counts number of detail files older than 30 days

sudo find /var/log/radius/radacct -type f -name "detail-*" -mtime +30 | wc -l

Details file space used in the directory

sudo du -sh /var/log/radius/radacct

Radius Log Access

The Radius Files are owned by the user:group of radiusd:radiusd But the Group does not natively have any read access (or executable access on the directory). So enable group read and executable access on the directories and read access on the log files and then add the relavant users to the radiusd group.

So that any new detail file will also allow the same access edit the following config file

sudo vi /etc/raddb/mods-enabled/detail

Apache, PHP and Microsoft SQL Driver install

Radius will use Rest to send requests to a webserver running PHP - which will insert the data into a SQL ddatabase.

Base Apache Install

Install Apache

sudo yum install httpd

Enable for Auto Startup

sudo systemctl enable httpd

Start Apache

sudo systemctl start httpd

Check Apache Status

sudo systemctl status httpd

Note by default Apache will be configured to run on port 80 - but we will change this to listen only on the loopback address on port 80 so only internal connections to apache can be made.

Use the below command to edit the httpd config file and change “Listen 80” to “Listen 127.0.0.1:80” and then restart the httpd service

sudo vi /etc/httpd/conf/httpd.conf

you can confirm this will no longer work by using netcat (nc) on any address other than 127.0.0.1 by simple testing 127.0.0.2 (this would have worked before this change) - example output below - where the first attempt fails:

[user]$ nc -zv 127.0.0.2 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Connection refused.
[user]$ nc -zv 127.0.0.1 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:80.
Ncat: 0 bytes sent, 0 bytes received in 0.01 seconds.
[user]$ nc -zv localhost 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:80.
Ncat: 0 bytes sent, 0 bytes received in 0.01 seconds.

Enabling HTTPS

Reference1: https://www.redhat.com/sysadmin/webserver-use-https
Reference2: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/5/html/deployment_guide/s1-httpd-secure-server

Redhat 8 Security guide - https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/securing-services_security-hardening#securing-apache-http-servers_securing-http-servers

//The mod_ssl configuration file is located at /etc/httpd/conf.d/ssl.conf. For this file to be loaded, and hence for mod_ssl to work, you must have the statement Include conf.d/*.conf in the /etc/httpd/conf/httpd.conf file. This statement is included by default in the default Apache HTTP Server configuration file. //

The SSL module install will have created it own SSL Cert with just the hostname and not the FQDN.
So you will need to create your CSR (and if you want a temp Self Signed Cert). The SSL Conf - ssl.conf file is located in /etc/httpd/conf.d/ directory You must also open the Firewall on port 443 and then reload the Firewall - as per below commands:

open Firewall on port 443

sudo firewall-cmd --permanent --add-port=443/tcp

Reload the Firewall

sudo firewall-cmd --reload

PHP 8.5 Install

A respository with PHP8.5 must be download and enabled below is an example of the repo TEMP_PHP8_5_PHP8_5_10

sudo dnf --enablerepo="TEMP_PHP8_5_PHP8_5_10" install php85

Install PHP system pasth (e.g. so you can run php from command line)

sudo dnf --enablerepo="TEMP_PHP8_5_PHP8_5_10" install php85-syspaths

Reminder

Upgrading PHP to latest versions

To check Updates are available:

sudo dnf check-update 'php*'

To install update

sudo dnf update 'php*'

Download, Install & Make MSODBC Drivers

sudo ACCEPT_EULA=Y dnf install -y msodbcsql18 unixODBC-devel gcc gcc-c++ make autoconf

libtool-ltdl is needed to be able to make pdo_sqlsrv successfully.

Dummy PHP webpage for testing Rest and reviewing the request via the PHP log files

this webpage just logs the request to log file and always gives back a 200 OK.

<?php
 
http_response_code(200);
header('Content-Type: application/json');
 
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
 
// Collect request headers
$headers = function_exists('getallheaders') ? getallheaders() : [];
 
if (!is_array($headers)) {
    $headers = [];
}
 
// Read the raw request body
$body = file_get_contents('php://input');
 
// Build a single-line JSON log entry
$log = [
    'time'    => date('c'),
    'ip'      => $ip,
    'method'  => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
    'uri'     => $_SERVER['REQUEST_URI'] ?? '/',
    'headers' => $headers,
    'body'    => $body,
];
 
// Log everything as one line
error_log(json_encode(
    $log,
    JSON_UNESCAPED_SLASHES |
    JSON_UNESCAPED_UNICODE |
    JSON_INVALID_UTF8_SUBSTITUTE
));
 
// Return a Successful response JSON object
 
// If we send below JSON - radius parses this and sends this back to the Gateway CUBE - this is probably a nice thing? You can see this Reply-Message in the *debug radius* output - but I have commented it out here.
// Note you **cannot** reply back with any values - this is Reply-Message is a standard RADIUS attribute (Attribute 18) defined by the protocol. i.e. if you do - you will get an error parising the response in radius - but it still shouldn't cause an issue.
// The "Reply-Message" response is a human-readable text message that the RADIUS server can send back to the NAS/client.
//echo '{"Reply-Message": "OK"}'; //
 
// in thie example we will just send back an empty 200 OK - this also means no parsing is done by Radius so no error occurs. The CUBE always get a positive response for the accounting record whatever happens. But if was set - we would ALSO send back to CUBE a Reply-Message OK.
 
// Send back a 200 OK (empty content)
http_response_code(200);
?>

Creating a CSR and importing the signed certificate to the webserver

Create a CSR Certificate

Below command saves the CSR file to the /home/user1/ directory. the Below also adds two SAN records - update as required (making one the same as the CN which you will fill in when prompted).

sudo openssl req -newkey rsa:2048 -addext "subjectAltName = DNS:myhomename.dmz.example.com, DNS:webchat.example.com" -nodes -keyout /etc/pki/tls/private/httpd.key -out /home/user1/httpd.csr

Just use

sudo more /home/user1/httpd.csr

to read the the CSR file and copy to a text file and request for it to be signed. Or use

 sudo chmod o+r /home/user1/httpd.csr

so that all (the other group) can read it and copy via WinSCP

Import Signed Cert

Copy the imported signed cert as /etc/pki/tls/certs/httpd.crt

sudo vi /etc/pki/tls/certs/httpd.crt

Configured Apache SSL to use this new Cert

Update the ssl.conf configuration file to utilise the certificate key and cert as per below.

sudo vi /etc/httpd/conf.d/ssl.conf

And edit the SSLCertificateFile and SSLCertificateKeyFile lines with the new relevant names - example below

SSLCertificateFile /etc/pki/tls/certs/httpd.crt
SSLCertificateKeyFile /etc/pki/tls/private/httpd.key

Restart the apache server.

sudo systemctl restart httpd

Adding a Monitor to Apache

If the Radius server is up - but Apache is down, the records won't be able to get added to the database, as we only try its local apache server.

To resolve this issue we will create a service monitor - which will stop the radius service if and when apache probe is unavailable. i.e. attempt every 10 seconds, and after 3 failed attempts shutdown the service. If we get 2 good attempts, we start it back up.

apache-radius-monitor.sh

apache-radius-monitor.service

apache-radius-monitor

Then run the following commands to install this service and monitor the log file

Troubleshooting

Basics - Check these

Check PHP error logs

sudo more /var/opt/remi/php85/log/php-fpm/www-error.log

Find which radius files have a certain string - example

sudo grep -rn "0034A582" /var/log/radius/radacct/

Check Radius Logs

sudo more /var/log/radius/radius.log
sudo ls -al /var/log/radius/

Run radiusd from command line and get debug output to the screen

First stop the service

 sudo systemctl stop radiusd

then start it from command line with the “-X” argument

sudo radiusd -X

SS command

Linux Socket statistics command - you can find what process are used for:

sudo ss -lunp | grep -E ':1812|:1813'
Part Meaning
sudo Runs the command with administrator privileges, needed to see process details for some sockets.
ss Stands for socket statistics. It displays network sockets and their state.
-l Shows listening sockets only.
-u Shows UDP sockets. RADIUS commonly uses UDP.
-n Shows numeric addresses and port numbers instead of resolving names.
-p Shows the process using each socket, when permitted.
grep -E Filters the output using an extended regular expression.
':1812' Matches lines containing port 1812

Example outpuut from this command

$  sudo ss -lunp | grep -E ":1813"
UNCONN 0      0            0.0.0.0:1813       0.0.0.0:*    users:(("radiusd",pid=848159,fd=37))

You could kill this process using the kill command, but you would only do this it was NOT possible to stop via systemctl

sudo kill -TTERM <PID>

Connecting to SQL Database

Allow Apache access to the network.

We first confirm that SELinux is enforced:

getenforce

If it is set to “Enforcing” we then confirm that httpd is NOT allowed access to the network:

getsebool httpd_can_network_connect

We then enable it

sudo setsebool -P httpd_can_network_connect on

Simple PHP file to confirm Read Access connectivity to the SQL database

sql-probe.php
<?php
 
require_once "./include/sql_db.php";
 
header('Content-Type: application/json');
http_response_code(200);
 
$response = [
    "Host"      => getDbHost(),
    "InService" => false
];
 
try {
    $pdo = getDbConnection();
 
    // Lightweight health check
    $stmt = $pdo->query("SELECT 1 AS Alive");
    $result = $stmt->fetch();
 
    if (!empty($result['Alive'])) {
        $response['InService'] = true;
    }
} catch (Throwable $e) {
    $response['sqlError'] = $e->getMessage();
}
 
echo json_encode($response, JSON_PRETTY_PRINT);
?>

And put the below file into a subfolder 'include'

sql_db.php
<?php
$host = "mysqldb.mydomain.com";
 
function getDbConnection(): PDO
{
	global $host;
    $database = "myDatabase";
    $username = "myUsername";
    $password = "myPa$$w0rd!";
 
    // Note we add TrustServerCertificate=true to the below - so we trust the SQL Server certificate, even if self signed etc.
    $dsn = "sqlsrv:Server={$host};Database={$database};TrustServerCertificate=true;LoginTimeout=5";
 
    return new PDO(
        $dsn,
        $username,
        $password,
        [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );
}
 
function getDbHost(): string
{
	global $host;
    return "$host";
}
?>

Appendix

Creating PFX cert file

A PFX (Personal Information Exchange) file is a password-protected binary container file based on the PKCS#12 standard that bundles a digital certificate (public key), its corresponding private key, and any intermediate or root certificates together in a single archive

Copying all Web files into one tar file