← All Tutorials

Securing ViciDial Admin Interface — HTTPS, IP Whitelist, 2FA

ViciDial Administration Intermediate 13 min read #67

Learn how to lock down your ViciDial admin panel with production-grade security: enforce HTTPS, restrict access by IP, implement two-factor authentication, and harden the database layer.

Prerequisites

Before implementing these security measures, ensure you have:

Verify your current setup:

mysql -V
apache2 -v
asterisk -V

Why Securing the Admin Interface Matters

The ViciDial admin interface (/vicidial/admin.php) controls:

Unauthorized access to the admin panel can result in:

This tutorial covers three complementary layers of defense.

Section 1: Enforce HTTPS and Disable HTTP

1.1 Obtain an SSL/TLS Certificate

If you don't have a certificate, obtain one using Let's Encrypt (free):

apt-get update
apt-get install certbot python3-certbot-apache -y
certbot certonly --standalone -d admin.yourdomain.com -d vicidial.yourdomain.com

Follow the prompts. Certificates are saved to /etc/letsencrypt/live/admin.yourdomain.com/.

For self-signed certificates (test environments only):

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/vicidial-self.key \
  -out /etc/ssl/certs/vicidial-self.crt \
  -subj "/C=US/ST=State/L=City/O=Company/CN=vicidial.local"

1.2 Configure Apache for HTTPS

Enable the SSL module:

a2enmod ssl
a2enmod rewrite
systemctl restart apache2

Locate your ViciDial VirtualHost configuration:

find /etc/apache2 -name "*vicidial*" -o -name "*.conf" | grep -i vicidial

If none exists, create one at /etc/apache2/sites-available/vicidial-ssl.conf:

<VirtualHost *:80>
    ServerName admin.yourdomain.com
    DocumentRoot /var/www/html
    
    # Force all HTTP traffic to HTTPS
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>

<VirtualHost *:443>
    ServerName admin.yourdomain.com
    DocumentRoot /var/www/html
    
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/admin.yourdomain.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/admin.yourdomain.com/privkey.pem
    
    # Modern SSL/TLS configuration
    SSLProtocol TLSv1.2 TLSv1.3
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
    SSLHonorCipherOrder on
    SSLCompression off
    
    # HSTS - Force HTTPS for 1 year
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    
    # Disable server signature exposure
    Header always unset "Server"
    Header always unset "X-Powered-By"
    ServerTokens Prod
    
    <Directory /var/www/html/vicidial>
        DirectoryIndex index.html admin.php
        AllowOverride All
        Require all granted
        
        # Disable directory listing
        Options -Indexes
    </Directory>
    
    # Proxy to Asterisk AGI if needed
    ProxyPreserveHost On
    <Location /agi-bin/>
        ProxyPass http://127.0.0.1:8088/agi-bin/
        ProxyPassReverse http://127.0.0.1:8088/agi-bin/
    </Location>
    
    ErrorLog ${APACHE_LOG_DIR}/vicidial-error.log
    CustomLog ${APACHE_LOG_DIR}/vicidial-access.log combined
</VirtualHost>

Enable the site:

a2ensite vicidial-ssl
apache2ctl configtest
systemctl restart apache2

Verify HTTPS is working:

curl -I https://admin.yourdomain.com 2>/dev/null | head -5

You should see 200 OK and HTTP/1.1 or HTTP/2.0.

1.3 Disable Weak Protocols and Ciphers

Verify your SSL configuration with testssl.sh or similar:

apt-get install testssl.sh
testssl.sh https://admin.yourdomain.com 2>/dev/null | grep -i "protocol\|cipher"

Remove any references to SSLv3, TLSv1.0, or TLSv1.1 from /etc/apache2/mods-available/ssl.conf:

SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'

Restart Apache:

systemctl restart apache2

Section 2: IP Whitelist for Admin Access

2.1 Implement IP Filtering at Apache Level

Edit /etc/apache2/sites-available/vicidial-ssl.conf to restrict access to /vicidial/admin.php:

<VirtualHost *:443>
    ServerName admin.yourdomain.com
    # ... SSL config as above ...
    
    <Location /vicidial/admin.php>
        # Whitelist office IPs
        Require ip 203.0.113.10
        Require ip 203.0.113.11
        Require ip 198.51.100.0/24
        
        # Log denied access attempts
        SetEnvIf Request_URI "/vicidial/admin.php" admin_access
    </Location>
    
    <Directory /var/www/html/vicidial>
        # ... other directives ...
    </Directory>
</VirtualHost>

# Log admin access to separate file
CustomLog ${APACHE_LOG_DIR}/vicidial-admin-access.log combined env=admin_access

Test the configuration:

apache2ctl configtest
systemctl restart apache2

Verify from an allowed IP:

curl -k https://admin.yourdomain.com/vicidial/admin.php 2>&1 | head -10

From a blocked IP, you should receive a 403 Forbidden error.

2.2 Create a Dynamic IP Whitelist File

For environments where admin IPs change frequently, use an include file:

Create /etc/apache2/vicidial-whitelist.conf:

<If "%{REQUEST_URI} =~ m#/vicidial/admin\.php#">
    Require ip 203.0.113.10 203.0.113.11
    Require ip 198.51.100.0/24
    Require ip 192.168.1.0/24
</If>

Include it in your VirtualHost:

<VirtualHost *:443>
    # ... SSL config ...
    
    Include /etc/apache2/vicidial-whitelist.conf
</VirtualHost>

To add IPs dynamically without restarting Apache:

echo "    203.0.113.99" >> /etc/apache2/vicidial-whitelist.conf
systemctl reload apache2

2.3 Add ModSecurity Rules (Optional but Recommended)

Install ModSecurity:

apt-get install libapache2-mod-security2 modsecurity-crs -y
a2enmod security2

Create /etc/apache2/conf-available/vicidial-modsecurity.conf:

SecRuleEngine On
SecRequestBodyAccess On
SecRequestBodyLimit 16777216
SecRequestBodyNoFiles Off

# Log all requests to admin interface
SecRule REQUEST_URI "@beginsWith /vicidial/admin.php" \
    "id:1001,phase:1,log,logdata:%{REMOTE_ADDR},pass"

# Block common injection attacks
SecRule ARGS "@rx (?:union|select|insert|update|delete|drop)" \
    "id:1002,phase:2,deny,status:403,msg:'SQL Injection attempt'"

SecRule ARGS "@rx (?:\.\./|\.\.\\)" \
    "id:1003,phase:2,deny,status:403,msg:'Path traversal attempt'"

Enable the module:

a2enconf vicidial-modsecurity
systemctl restart apache2

Monitor ModSecurity logs:

tail -f /var/log/apache2/modsec_audit.log | grep -i vicidial

Section 3: Implement Two-Factor Authentication (2FA)

3.1 Choose a 2FA Method

ViciDial does not include native 2FA in the admin interface. We'll implement 2FA by:

  1. Database-level tracking of OTP secrets and verified states
  2. PHP wrapper that intercepts login attempts
  3. TOTP generation using Google Authenticator or Authy

3.2 Prepare the Database

Create a new table to store TOTP secrets and recovery codes:

USE asterisk;

CREATE TABLE IF NOT EXISTS `vicidial_admin_2fa` (
    `user_id` INT NOT NULL,
    `username` VARCHAR(20) NOT NULL,
    `totp_secret` VARCHAR(32),
    `totp_enabled` ENUM('Y','N') DEFAULT 'N',
    `backup_codes` TEXT,
    `created_date` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `last_verified` DATETIME,
    PRIMARY KEY (`user_id`),
    FOREIGN KEY (`user_id`) REFERENCES `vicidial_users`(`user_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Add 2FA attempt logging
CREATE TABLE IF NOT EXISTS `vicidial_2fa_audit` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `username` VARCHAR(20) NOT NULL,
    `attempt_type` ENUM('LOGIN','OTP_VERIFY','BACKUP_CODE') NOT NULL,
    `result` ENUM('SUCCESS','FAILURE','INVALID_USER') NOT NULL,
    `remote_ip` VARCHAR(45),
    `attempt_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX (`username`),
    INDEX (`attempt_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Verify table creation:

mysql -u root -p asterisk -e "DESCRIBE vicidial_admin_2fa;"

3.3 Install PHP TOTP Library

Navigate to the ViciDial web root:

cd /var/www/html/vicidial

Install PHPGangsta_GoogleAuthenticator (TOTP implementation):

apt-get install php-composer -y
composer require phpgangsta/googleauthenticator 1.0.*

Or download manually:

curl -L -o /var/www/html/vicidial/GoogleAuthenticator.php \
  https://raw.githubusercontent.com/PHPGangsta/GoogleAuthenticator/master/PHPGangsta/GoogleAuthenticator.php

Verify installation:

ls -la /var/www/html/vicidial/GoogleAuthenticator.php

3.4 Create 2FA Verification Script

Create /var/www/html/vicidial/admin_2fa_login.php:

<?php
/**
 * ViciDial Admin 2FA Login Handler
 * Validates TOTP before granting access to admin.php
 */

session_start();
error_reporting(0);

// Database configuration
$db_host = 'localhost';
$db_user = 'root';
$db_pass = 'YOUR_DB_PASSWORD';
$db_name = 'asterisk';

$mysqli = new mysqli($db_host, $db_user, $db_pass, $db_name);
if ($mysqli->connect_error) {
    die('Database connection failed: ' . $mysqli->connect_error);
}

// Include TOTP library
require_once 'GoogleAuthenticator.php';
$ga = new PHPGangsta_GoogleAuthenticator();

$username = '';
$error_msg = '';
$info_msg = '';

// Handle OTP verification
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['action']) && $_POST['action'] === 'verify_otp') {
        
        $username = sanitizeInput($_POST['username'] ?? '');
        $otp_code = sanitizeInput($_POST['otp_code'] ?? '');
        $remote_ip = $_SERVER['REMOTE_ADDR'];
        
        // Validate inputs
        if (empty($username) || empty($otp_code)) {
            $error_msg = 'Username and OTP code required';
            logAuditEvent($mysqli, $username, 'OTP_VERIFY', 'FAILURE', $remote_ip, 'Empty fields');
        } else {
            // Fetch TOTP secret from database
            $stmt = $mysqli->prepare("
                SELECT totp_secret FROM vicidial_admin_2fa 
                WHERE username = ? AND totp_enabled = 'Y'
            ");
            $stmt->bind_param('s', $username);
            $stmt->execute();
            $result = $stmt->get_result();
            
            if ($result->num_rows === 0) {
                $error_msg = 'User not found or 2FA not enabled';
                logAuditEvent($mysqli, $username, 'OTP_VERIFY', 'INVALID_USER', $remote_ip);
            } else {
                $row = $result->fetch_assoc();
                $totp_secret = $row['totp_secret'];
                
                // Verify the OTP (allows 1 minute window for clock drift)
                $checkResult = $ga->verifyCode($totp_secret, $otp_code, 1);
                
                if ($checkResult) {
                    // OTP valid - allow access
                    $_SESSION['2fa_verified'] = true;
                    $_SESSION['admin_user'] = $username;
                    $_SESSION['2fa_verify_time'] = time();
                    
                    // Update last verified timestamp
                    $stmt = $mysqli->prepare("
                        UPDATE vicidial_admin_2fa 
                        SET last_verified = NOW() 
                        WHERE username = ?
                    ");
                    $stmt->bind_param('s', $username);
                    $stmt->execute();
                    
                    logAuditEvent($mysqli, $username, 'OTP_VERIFY', 'SUCCESS', $remote_ip);
                    
                    // Redirect to admin.php
                    header('Location: admin.php');
                    exit;
                } else {
                    $error_msg = 'Invalid OTP code. Please try again.';
                    logAuditEvent($mysqli, $username, 'OTP_VERIFY', 'FAILURE', $remote_ip, 'Invalid OTP');
                }
            }
            $stmt->close();
        }
    }
}

function sanitizeInput($input) {
    return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}

function logAuditEvent($mysqli, $username, $type, $result, $ip, $notes = '') {
    $stmt = $mysqli->prepare("
        INSERT INTO vicidial_2fa_audit (username, attempt_type, result, remote_ip) 
        VALUES (?, ?, ?, ?)
    ");
    $stmt->bind_param('ssss', $username, $type, $result, $ip);
    $stmt->execute();
    $stmt->close();
}

?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ViciDial Admin - Two-Factor Authentication</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .login-container {
            background: white;
            padding: 40px;
            border-radius: 8px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            width: 100%;
            max-width: 400px;
        }
        .login-container h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: bold;
        }
        input[type="text"],
        input[type="password"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
            font-size: 16px;
        }
        input[type="text"]:focus,
        input[type="password"]:focus {
            outline: none;
            border-color: #667eea;
            box-shadow: 0 0 5px rgba(102,126,234,0.3);
        }
        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #fee;
            color: #c33;
            padding: 12px;
            border-radius: 4px;
            margin-bottom: 20px;
            border-left: 4px solid #c33;
        }
        .info {
            background: #e3f2fd;
            color: #1976d2;
            padding: 12px;
            border-radius: 4px;
            margin-bottom: 20px;
            border-left: 4px solid #1976d2;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h1>🔐 ViciDial Admin</h1>
        
        <?php if ($error_msg): ?>
            <div class="error"><?php echo $error_msg; ?></div>
        <?php endif; ?>
        
        <?php if ($info_msg): ?>
            <div class="info"><?php echo $info_msg; ?></div>
        <?php endif; ?>
        
        <form method="POST">
            <input type="hidden" name="action" value="verify_otp">
            
            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" id="username" name="username" placeholder="Admin username" required autofocus>
            </div>
            
            <div class="form-group">
                <label for="otp_code">Authenticator Code</label>
                <input type="text" id="otp_code" name="otp_code" placeholder="000000" maxlength="6" pattern="\d{6}" required>
                <div class="info" style="margin-top: 8px; margin-bottom: 0;">
                    Enter the 6-digit code from your authenticator app (Google Authenticator, Authy, etc.)
                </div>
            </div>
            
            <button type="submit">Verify & Access Admin Panel</button>
        </form>
    </div>
</body>
</html>

Set proper permissions:

chown www-data:www-data /var/www/html/vicidial/admin_2fa_login.php
chmod 640 /var/www/html/vicidial/admin_2fa_login.php

3.5 Wrap the Existing admin.php to Enforce 2FA

Create a backup of the original admin.php:

cp /var/www/html/vicidial/admin.php /var/www/html/vicidial/admin.php.bak

Create /var/www/html/vicidial/admin_2fa_check.php (new wrapper):

<?php
/**
 * 2FA Check Wrapper for ViciDial Admin
 * Must be included at the top of admin.php
 */

session_start();

$config = array(
    'db_host' => 'localhost',
    'db_user' => 'root',
    'db_pass' => 'YOUR_DB_PASSWORD',
    'db_name' => 'asterisk',
    '2fa_enabled' => true,  // Set to false to disable 2FA globally
    '2fa_session_timeout' => 3600  // 1 hour in seconds
);

// List of admin usernames that MUST use 2FA
$mandatory_2fa_users = array(
    'admin',
    'root',
    'superadmin'
);

// Check if user is already verified in this session
if (isset($_SESSION['2fa_verified']) && isset($_SESSION['admin_user'])) {
    // Check session timeout
    if (isset($_SESSION['2fa_verify_time'])) {
        if ((time() - $_SESSION['2fa_verify_time']) < $config['2fa_session_timeout']) {
            // Session still valid - continue to admin.php
            return true;
        } else {
            // Session expired - force re-authentication
            session_destroy();
            header('Location: admin_2fa_login.php');
            exit;
        }
    }
}

// User not yet verified - redirect to 2FA login
if ($config['2fa_enabled']) {
    // Check if accessing admin.php directly
    if (basename($_SERVER['PHP_SELF']) === 'admin.php') {
        header('Location: admin_2fa_login.php');
        exit;
    }
}

Now, modify the original /var/www/html/vicidial/admin.php to include the 2FA check at the very top (after <?php):

<?php
// Add this immediately after opening PHP tag
require_once('admin_2fa_check.php');

// ... rest of original admin.php code ...

Or, create a new admin.php that acts as a wrapper:

<?php
require_once('admin_2fa_check.php');
include('admin.php.bak');
?>

3.6 Generate QR Codes for User Enrollment

Create /var/www/html/vicidial/setup_2fa.php (accessible only to logged-in admins):

<?php
session_start();
error_reporting(0);

$db_host = 'localhost';
$db_user = 'root';
$db_pass = 'YOUR_DB_PASSWORD';
$db_name = 'asterisk';

$mysqli = new mysqli($db_host, $db_user, $db_pass, $db_name);

require_once 'GoogleAuthenticator.php';
$ga = new PHPGangsta_GoogleAuthenticator();

// Only accessible via POST with proper session
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    die('Invalid request');
}

// Generate new TOTP secret
$secret = $ga->createSecret();
$username = sanitizeInput($_POST['username'] ?? '');

if (!$username) {
    die('Username required');
}

// Generate QR code URL
$qr_url = $ga->getQRCodeGoogleUrl('ViciDial Admin', $secret);

// Generate backup codes (10 codes)
$backup_codes = array();
for ($i = 0; $i < 10; $i++) {
    $backup_codes[] = bin2hex(random_bytes(4));
}
$backup_codes_json = json_encode($backup_codes);

// Store in database (not yet enabled until user confirms)
$stmt = $mysqli->prepare("
    INSERT INTO vicidial_admin_2fa (username, totp_secret, backup_codes, totp_enabled)
    VALUES (?, ?, ?, 'N')
    ON DUPLICATE KEY UPDATE
        totp_secret = VALUES(totp_secret),
        backup_codes = VALUES(backup_codes),
        totp_enabled = 'N'
");
$stmt->bind_param('sss', $username, $secret, $backup_codes_json);
$stmt->execute();
$stmt->close();

// Return JSON response
header('Content-Type: application/json');
echo json_encode(array(
    'success' => true,
    'qr_code_url' => $qr_url,
    'secret' => $secret,
    'backup_codes' => $backup_codes
));

function sanitizeInput($input) {
    return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
?>

3.7 Enable 2FA for Specific Users

Run this SQL to enable 2FA for critical admin accounts:

UPDATE vicidial_admin_2fa 
SET totp_enabled = 'Y' 
WHERE username IN ('admin', 'superadmin');

-- View current 2FA status
SELECT username, totp_enabled, last_verified, created_date 
FROM vicidial_admin_2fa 
ORDER BY created_date DESC;

Monitor 2FA login attempts:

mysql -u root -p asterisk -e "
SELECT username, attempt_type, result, remote_ip, attempt_time 
FROM vicidial_2fa_audit 
WHERE attempt_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY attempt_time DESC;
"

Section 4: Harden Database and Related Access

4.1 Restrict MySQL Access

By default, MySQL listens on all interfaces. Restrict it to localhost:

Edit /etc/mysql/mysql.conf.d/mysqld.cnf:

[mysqld]
bind-address = 127.0.0.1
skip-external-locking

# Disable DNS resolution (faster, more secure)
skip-name-resolve

# Disable file-based access
skip-load-data-local-infile

Restart MySQL:

systemctl restart mysql

4.2 Create Least-Privilege Database User

Instead of using root for the web application, create a dedicated user:

-- Create new user for web app
CREATE USER 'vicidial_web'@'localhost' IDENTIFIED BY 'COMPLEX_PASSWORD_HERE';

-- Grant minimal privileges
GRANT SELECT, INSERT, UPDATE ON asterisk.* TO 'vicidial_web'@'localhost';
GRANT SELECT ON asterisk.vicidial_users TO 'vicidial_web'@'localhost';
GRANT SELECT ON asterisk.vicidial_admin_2fa TO 'vicidial_web'@'localhost';
GRANT INSERT ON asterisk.vicidial_2fa_audit TO 'vicidial_web'@'localhost';

-- Apply changes
FLUSH PRIVILEGES;

-- Verify
SELECT user, host, authentication_string FROM mysql.user WHERE user = 'vicidial_web';
SHOW GRANTS FOR 'vicidial_web'@'localhost';

Update your PHP scripts to use this user instead of root.

4.3 Log All Admin Database Queries

Enable query logging for the asterisk database:

SET GLOBAL general_log = 'ON';
SET GLOBAL log_output = 'TABLE';

Query log is stored in mysql.general_log table. To export:

mysql -u root -p asterisk -e "
SELECT event_time, user_host, command_type, argument 
FROM mysql.general_log 
WHERE db = 'asterisk' AND command_type IN ('Query', 'Execute')
LIMIT 100;
" > /var/log/vicidial-db-queries.log

To disable general log (after investigation):

SET GLOBAL general_log = 'OFF';

Section 5: Monitoring and Logging

5.1 Configure Centralized Logging

Create /var/www/html/vicidial/log_admin_events.php:

<?php
/**
 * Log all admin access events to file and database
 */

function logAdminEvent($event_type, $username, $action, $details = array()) {
    $log_file = '/var/log/vicidial/admin-events.log';
    
    // Ensure log directory exists
    if (!is_dir('/var/log/vicidial')) {
        mkdir('/var/log/vicidial', 0750, true);
        chown('/var/log/vicidial', 'www-data');
    }
    
    $timestamp = date('Y-m-d H:i:s');
    $remote_ip = $_SERVER['REMOTE_ADDR'];
    $user_agent = substr($_SERVER['HTTP_USER_AGENT'] ?? 'Unknown', 0, 100);
    
    $log_entry = sprintf(
        "[%s] Type:%s | User:%s | Action:%s | IP:%s | UA:%s | Details:%s\n",
        $timestamp,
        $event_type,
        $username,
        $action,
        $remote_ip,
        $user_agent,
        json_encode($details)
    );
    
    // Write to file
    file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
    
    // Also log to syslog
    openlog('vicidial-admin', LOG_PID | LOG_PERROR, LOG_LOCAL0);
    sys

Stuck on something specific?

Book a free 30-minute call. I run ViciDial centers across 3 countries and can usually unblock your setup in one session — or build it for you.

Book a Free Consultation