This is an old revision of the document!
Radius VoIP Calls Application
Purpose & Scope
- 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.)
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)
- Install freeradius
sudo yum install freeradius
- Enable Service
sudo systemctl enable radiusd
- 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')
sudo rm /etc/raddb/sites-enabled/inner-tunnel
sudo rm /etc/raddb/sites-enabled/default
- Add custom config file cube-accounting - i.e. use VI and copy and paste in the config and save the file
- note this sample config does NOT contain any REST request - but this will get it accounting records working
sudo vi /etc/raddb/sites-available/cube-accounting
server cube-accounting { listen { type = acct ipaddr = * port = 1813 proto = udp } recv Accounting-Request { accounting } accounting { detail } }
- Fix ownership of the above config file:
sudo chown root:radiusd /etc/raddb/sites-available/cube-accounting
- add symlink for config file
sudo ln -s /etc/raddb/sites-available/cube-accounting /etc/raddb/sites-enabled/cube-accounting
- Fix ownership of the symlink - note the -h
sudo chown -h root:radiusd /etc/raddb/sites-enabled/cube-accounting
- Remove the EAP module (as we won't be using it - and it will causes an config check error
sudo rm /etc/raddb/mods-enabled/eap
- Then check config is good
sudo radiusd -XC
- And start freeradius (and then check its status)
sudo systemctl start radiusd
sudo systemctl status radiusd
- Open Radius Accounting port - UDP 1813
sudo firewall-cmd --permanent --add-port=1813/udp
- Reload Firewall to make config active
sudo firewall-cmd --reload
- 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
- Comment out the client localhost AND client localhost_ipv6 sections
sudo vi /etc/raddb/clients.conf
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.
- 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
| Value | Cisco's definition | Interpretation |
|---|---|---|
| 1 | Calling party located in PSTN | Calling party on the PSTN side released the call |
| 2 | Calling party located in VoIP network | Calling party on the VoIP side released the call |
| 3 | Called party located in PSTN | Called party on the PSTN side released the call |
| 4 | Called party located in VoIP network | Called 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
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:
- 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
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.
- 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
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. //
- 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 the following file and copy to your home folder via WinSCP
- Move to tmp folder
sudo mv libtool-ltdl-devel-2.4.6-46.el9.x86_64.rpm /tmp
- Install
sudo dnf install /tmp/libtool-ltdl-devel-2.4.6-46.el9.x86_64.rpm
- 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
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
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
Troubleshooting
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
- 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
Copying all Web files into one tar file
- 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/