This is an old revision of the document!


Radius VoIP Calls Application

  • Record in real time, when a call is established (connected) when entering or leaving a CUBE to/from a PSTN provider to a SQL database.
  • Include specific details about the call.
  • Add additional information if and when the call is connected to a Contact Centre agent using a Finesse headless Gadget
  • When the call disconnects, update the row (with the disconnect time, that the call is no longer active etc.)

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

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
!

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

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

Install the Rest Module

sudo dnf install freeradius-rest

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

  • Acct-Status-Type = Start
  • Acct-Status-Type = Interim-Update
  • Acct-Status-Type = Stop
h323-gw-id

The hostname of the CUBE

  • h323-gw-id = “h323-gw-id=CUBE-A.example.org”
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-conf-id = “h323-conf-id=0B31D45A C6ED11F1 8491B15D B7BDE126”
h323-call-origin

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

  • h323-call-origin = “h323-call-origin=answer”
    • This signifies an incoming dial-peer (i.e. Gateway answered / received it)
  • h323-call-origin = “h323-call-origin=originate”
    • this signfies an outgoing dial-peer (i.e. it is originating FROM the Gateway)
Called-Station-Id

The Called Party Number

  • Called-Station-Id = “+35312612800”
User-Name

The Calling Party Number

  • User-Name = “+35312399138”
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.

  • NAS-IP-Address = 10.123.123.22
Timestamp

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

  • Timestamp = 1791972105
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'

  • Cisco-AVPair = “release-source=2”
    • Calling party on the VoIP side released the call
  • Cisco-AVPair = “release-source=4”
    • Called party on the VoIP side released the call
Other key fields

Note - only available on 'Stop' Records

  • 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”
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

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:

  • Create a daily cron job
    • sudo vi /etc/cron.daily/radiusd-detail-cleanup
    • #!/bin/bash
      
      find /var/log/radius/radacct -type f -name "detail-*" -mtime +30 -delete
  • Make it executable
    • sudo chmod 755 /etc/cron.daily/radiusd-detail-cleanup

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

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.

  • Add read+execute access to all directories and sub directories of /var/log/radius
    • sudo find /var/log/radius -type d -exec chmod g+rx {} +
  • Add read access to all files (recussive) /var/log/radius
    • sudo find /var/log/radius -type f -exec chmod g+r {} +
  • Add a user to the group radiusd (note - you need to log out and log back in for this to take affect)
    • sudo usermod -aG radiusd <user>
  • To check the users who are in the group radiusd
    • getent group radiusd

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

sudo vi /etc/raddb/mods-enabled/detail
  • And change the line (which currently only has read/write access for user and no access for the group:
    • permissions = 0600
    • to be (which will give read access to the group):
    • permissions = 0640
    • Restart radiusd service:
    • systemctl restart radiusd

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

  • Install the SSL for httpd -
    sudo yum install httpd mod_ssl
  • Restart apache -
    sudo systemctl restart httpd

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

  • HTTP Files are located here:
    • /var/www/html/
  • PHP FPM Config
    • /etc/opt/remi/php85/php-fpm.conf
  • PHP Error log
    • /var/opt/remi/php85/log/php-fpm/error.log
  • PHP WWW Error log (when php code logs to error log)
    • /var/opt/remi/php85/log/php-fpm/www-error.log

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.

  • Download latest version pdo_sqlsrv file which supports PHP 8.5 (at the time of writing this is the file below)
    • https://github.com/microsoft/msphpsql/releases/download/v5.13.3/pdo_sqlsrv-5.13.3.tgz
  • Extract the file
    • tar -xzf pdo_sqlsrv-5.13.3.tgz
  • Enter the Directory
    • cd pdo_sqlsrv-5.13.3
  • When Before runnign the below phpize command - it failed as I did not have the php85-php-devel module installed
    • sudo dnf list --installed 'php*'
    • sudo dnf --enablerepo="TEMP_PHP8_5_PHP8_5_10" install php85-php-devel
  • Run this command to make the config file (check the file configure is created)
    • /opt/remi/php85/root/usr/bin/phpize
  • Run
    • ./configure --with-php-config=/opt/remi/php85/root/usr/bin/php-config
    • make
    • sudo make install
  • Add pdo_sqlsrv to PHP start config
    • sudo bash -c 'echo "extension=pdo_sqlsrv.so" > /etc/opt/remi/php85/php.d/40-pdo_sqlsrv.ini'
  • Restart PHP Fast process Manager (fpm)
    • sudo systemctl restart php85-php-fpm
  • Fix permission on php-fpm log folder
    • sudo chmod o+xr /var/opt/remi/php85/log/php-fpm
  • Change log rotate from the default of weekly to rotate every day and purge after 30 rotations (30 days)
    • sudo vi /etc/logrotate.d/php85-php-fpm
    • This is the default file:
    • /var/opt/remi/php85/log/php-fpm/*log {
          missingok
          notifempty
          sharedscripts
          delaycompress
          postrotate
              /bin/kill -SIGUSR1 `cat /var/opt/remi/php85/run/php-fpm/php-fpm.pid 2>/dev/null` 2>/dev/null || true
          endscript
      }
  • This is the updated version:
    • /var/opt/remi/php85/log/php-fpm/*log {
          daily
          rotate 30
      
          missingok
          notifempty
          sharedscripts
      
          nocompress
          dateext
      
          create
      
          postrotate
              /bin/kill -SIGUSR1 `cat /var/opt/remi/php85/run/php-fpm/php-fpm.pid 2>/dev/null` 2>/dev/null || true
          endscript
      }
  • after changing you can test safely (logs details to console) logtate using the following commands:
    • sudo logrotate -d /etc/logrotate.conf
  • And you can force a logrtate using:
    • sudo logrotate -f /etc/logrotate.conf

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);
?>

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

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

  • sudo vi /usr/local/sbin/apache-radius-monitor.sh
  • #!/bin/bash
    #
    # apache-radius-monitor.sh
    #
    # Probes Apache every INTERVAL seconds. After FAIL_THRESHOLD consecutive failed
    # probes it stops radiusd (so the CUBE fails over to the secondary RADIUS server).
    # After OK_THRESHOLD consecutive good probes it starts radiusd again, but ONLY if
    # this script was the one that stopped it (a radiusd you stopped by hand stays stopped).
    #
    # Install to: /usr/local/sbin/apache-radius-monitor.sh  (chmod 755)
    
    # ---------------- Settings ----------------
    URL="http://localhost/api/probe.php"
    INTERVAL=10            # seconds between probes
    CURL_TIMEOUT=3         # seconds before a probe counts as failed
    FAIL_THRESHOLD=3       # consecutive failures before stopping radiusd (~30s)
    OK_THRESHOLD=2         # consecutive successes before starting radiusd (~20s)
    SERVICE="radiusd"
    LOGFILE="/var/log/apache-radius-monitor.log"
    STATE_FILE="/run/apache-radius-monitor.stopped"   # exists only if WE stopped radiusd
    MAIL_TO=""             # e.g. "[email protected]". Leave empty to disable email alerts.
    # ------------------------------------------
    
    log() {
        echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOGFILE"
    }
    
    alert() {
        log "$1"
        if [ -n "$MAIL_TO" ] && command -v mail >/dev/null 2>&1; then
            echo "$1" | timeout 30 mail -s "$(hostname -s): $1" "$MAIL_TO" || log "WARNING: email send failed"
        fi
    }
    
    probe() {
        code=$(curl -s -o /dev/null -w '%{http_code}' --max-time "$CURL_TIMEOUT" "$URL")
        [ "$code" = "200" ]
    }
    
    fails=0
    oks=0
    
    log "Monitor started: url=$URL interval=${INTERVAL}s stop_after=${FAIL_THRESHOLD} start_after=${OK_THRESHOLD}"
    
    while true; do
        cycle_start=$SECONDS
    
        if probe; then
            fails=0
            if [ -f "$STATE_FILE" ]; then
                oks=$((oks + 1))
                log "Probe OK ($oks/$OK_THRESHOLD) while $SERVICE is stopped by monitor"
                if [ "$oks" -ge "$OK_THRESHOLD" ]; then
                    if systemctl start "$SERVICE"; then
                        rm -f "$STATE_FILE"
                        oks=0
                        alert "Apache recovered - $SERVICE started"
                    else
                        alert "Apache recovered but FAILED to start $SERVICE (will retry)"
                    fi
                fi
            else
                oks=0
            fi
        else
            oks=0
            fails=$((fails + 1))
            log "Probe FAILED (HTTP ${code:-000}) $fails/$FAIL_THRESHOLD"
            if [ "$fails" -ge "$FAIL_THRESHOLD" ] && [ ! -f "$STATE_FILE" ]; then
                if systemctl is-active --quiet "$SERVICE"; then
                    if systemctl stop "$SERVICE"; then
                        touch "$STATE_FILE"
                        alert "Apache down - $SERVICE stopped"
                    else
                        alert "Apache down but FAILED to stop $SERVICE (will retry)"
                    fi
                fi
                # If $SERVICE is not active (e.g. stopped by an admin) leave it alone.
            fi
        fi
    
        elapsed=$((SECONDS - cycle_start))
        remaining=$((INTERVAL - elapsed))
        if [ "$remaining" -gt 0 ]; then
            sleep "$remaining"
        fi
    done
  • sudo chmod 755 /usr/local/sbin/apache-radius-monitor.sh

apache-radius-monitor.service

  • sudo vi /etc/systemd/system/apache-radius-monitor.service
  • [Unit]
    Description=Apache probe monitor - stops/starts radiusd for RADIUS failover
    After=network.target httpd.service radiusd.service
    
    [Service]
    Type=simple
    ExecStart=/usr/local/sbin/apache-radius-monitor.sh
    Restart=always
    RestartSec=5
    User=root
    
    [Install]
    WantedBy=multi-user.target
  • sudo chmod 644 /etc/systemd/system/apache-radius-monitor.service

apache-radius-monitor

  • sudo vi /etc/logrotate.d/apache-radius-monitor
  • /var/log/apache-radius-monitor.log {
        weekly
        rotate 8
        compress
        missingok
        notifempty
        copytruncate
    }
  • sudo chmod 644 /etc/logrotate.d/apache-radius-monitor

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

  • sudo systemctl daemon-reload
  • sudo systemctl enable --now apache-radius-monitor
  • tail -f /var/log/apache-radius-monitor.log

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/

First stop the service

 sudo systemctl stop radiusd

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

sudo radiusd -X

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>

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";
}
?>

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

  • Before running the below 'openssl' command - check the actual private key file and crt file used by the web server
    • more /etc/httpd/conf.d/ssl.conf
  • Create the PKI file
    • sudo openssl pkcs12 -export -out ~/radius-server-a.pfx -inkey /etc/pki/tls/private/httpd_20261029.key -in /etc/pki/tls/certs/httpd_20261029.crt
  • Enter and note the password used to secure the file
  • Go to your home directory /user/home/<user>/ (or use the shortcut “~”) & make file readable by your user account (readable by all)
    • cd ~
    • sudo chmod 644 radius-server-a.pfx
  • Copy it using WinSCP to your windows machine
  • enter the html file directory
    • sudo cd /var/www/html
  • Create a tar file in your home directory called web.tar of the current directory (c = Create archive, v = show files been archived, f - specifcy archive filename) (add z if you want to compress and then add .gz to filename)
    • sudo tar -cvf ~/web.tar ./
  • Update permissions so you can copy via WinSCP
    • sudo chomd 644 ~/web.tar
  • Use WinSCP to copy from and to another server - then extract using the following command:
    • sudo tar -xvf web.tar -C /var/www/html/
  • vendors/cisco/routers/radius.1794321766.txt.gz
  • Last modified: 2026/11/10 14:42
  • by gerardorourke