Dynamic IP address and the white-list
Keeping the white-list up to date with a dynamic IP address
CyberGate only accepts SIP traffic from public WAN IP addresses on the white-list of your tenant (see CyberGate Network RequirementsCyberGate Network Requirements). Some internet providers (ISPs) do not give a site a fixed public IP address. The address changes now and then, for example after a router restart or when the provider renews the lease.
When that happens, the new address is not on the white-list. The intercoms at that site can no longer register with CyberGate, and calls stop working until someone adds the new address in the portal.
You can automate that step with the CyberGate Customer API. This page describes how, and includes an example script in Python.
A fixed public IP address is still the most reliable option. If your ISP offers one, ask for it first. Use the approach on this page when that is not possible.
How it works
A small script runs on a computer at the site, every few minutes. Each time it runs, it:
- Asks an external service which public IP address the site uses on the internet.
- Reads the white-list through the Customer API and looks up the entry for this site.
- Stops when the address has not changed.
- Replaces the old address with the new one when it has changed.
The script changes one existing white-list entry in place. It never adds or deletes entries. A single API call replaces the old address with the new one, so there is no moment where both addresses or neither address is on the white-list.
Updating in place also matters because CyberGate limits the number of IP addresses on the white-list of a tenant. Adding the new address before removing the old one can fail when you are at that limit.
What you need
- An API secret, created on the API Access page in the CyberGate Management Portal. See CyberGate API Access - in depthCyberGate API Access - in depth.
- A computer at the site that is always on and uses the same internet connection as the intercoms. The script detects the public IP address of the computer it runs on, so that has to be the address the intercoms use. A small Linux machine, a Raspberry Pi or a Windows server all work.
- Python 3.8 or newer on that computer. The script only uses the Python standard library, so there is nothing else to install.
One-time setup
- In the CyberGate Management Portal, go to Network and click Add WAN IP Address or Range. Enter the current public IP address of the site as a single address. Give it a description that says what it is, for example Office Utrecht (dynamic IP).
- Save the script below on the computer at the site as update_dynamic_ip_whitelist.py.
- Set the API secret as an environment variable (see Running the script below) and look up the ID of the entry you created in step 1. The portal does not show IDs, so let the script list them:
python3 update_dynamic_ip_whitelist.py --listThe output shows every white-list entry with its ID, IP addresses, description and location.
- Open the script and enter that ID as the value of WHITELIST_ENTRY_ID in the configuration section.
- Test the setup without changing anything:
python3 update_dynamic_ip_whitelist.py --dry-runThe example script
#!/usr/bin/env python3
"""
CyberGate Customer API example: keep the SIP IP white-list in sync with a dynamic public IP address.
Use case
--------
Your intercoms connect to CyberGate from a site whose internet provider (ISP) assigns a dynamic
public IP address. CyberGate only accepts SIP traffic from white-listed IP addresses, so each time
the ISP hands out a new address, the white-list has to be updated.
Run this script periodically on a machine inside that site (for example every 5 minutes). It:
1. Determines the current public IP address of the site.
2. Reads the SIP white-list through the CyberGate Customer API and looks up the entry with the
configured ID.
3. Does nothing when the IP address has not changed.
4. Updates the entry to the new IP address when it has changed.
The update replaces the old address with the new one in a single call, so there is no moment
where both or neither of the addresses are white-listed.
The script only updates an existing entry, it never creates or deletes entries.
Requirements
------------
- Python 3.8 or newer. Only the Python standard library is used, nothing needs to be installed.
- A Customer API secret, created in the CyberGate portal.
One-time setup
--------------
1. In the CyberGate portal, go to the network settings and add a white-list entry for the site,
with the current public IP address as both the start and the end address.
2. Look up the ID of that entry. The portal does not show IDs, so let this script list them:
python3 update_dynamic_ip_whitelist.py --list
3. Enter that ID as WHITELIST_ENTRY_ID in the configuration section below.
Usage
-----
Linux / macOS:
export CYBERGATE_API_SECRET="<your API secret>"
python3 update_dynamic_ip_whitelist.py
Windows (PowerShell):
$env:CYBERGATE_API_SECRET = "<your API secret>"
python update_dynamic_ip_whitelist.py
Options:
--list show all white-list entries with their ID and exit
--dry-run show what would change, without changing anything
Scheduling
----------
Linux / macOS (cron), every 5 minutes, with the output appended to a log file:
*/5 * * * * CYBERGATE_API_SECRET=<secret> python3 /opt/cybergate/update_dynamic_ip_whitelist.py \
>> /var/log/cybergate-ip.log 2>&1
(In crontab, write this on a single line, without the backslash.)
Windows: create a task in Task Scheduler that runs python.exe with this script as argument,
repeated every 5 minutes, and set CYBERGATE_API_SECRET as a system environment variable.
Keep the interval at a few minutes or more: the Customer API is rate limited per IP address.
"""
import argparse
import ipaddress
import json
import os
import sys
import urllib.error
import urllib.request
# ---------------------------------------------------------------------------------------------
# Configuration: adjust these values to your situation.
# ---------------------------------------------------------------------------------------------
# Base URL of the CyberGate Customer API.
API_BASE_URL = "https://admin.cybergate.cybertwice.com/api/customer"
# The API secret is read from an environment variable, so it does not end up in this file.
API_SECRET = os.environ.get("CYBERGATE_API_SECRET", "")
# ID of the white-list entry this script keeps up to date. Find it with: --list
WHITELIST_ENTRY_ID = 0
# Service that returns the public IPv4 address of the caller as plain text.
# Alternatives: https://ipv4.icanhazip.com or https://checkip.amazonaws.com
PUBLIC_IP_SERVICE_URL = "https://api.ipify.org"
# Timeout in seconds for every HTTP request.
HTTP_TIMEOUT_SECONDS = 15
# ---------------------------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------------------------
def get_public_ip_address():
"""
Asks an external service which public IP address this site uses on the internet.
The service sees the address our request comes from, which is the address the ISP assigned
to the site router. That is also the address the intercoms use to connect to CyberGate.
The result is validated, so an error page from the service is never written to the white-list.
"""
request = urllib.request.Request(PUBLIC_IP_SERVICE_URL, headers={"User-Agent": "CyberGate-IP-Updater"})
with urllib.request.urlopen(request, timeout=HTTP_TIMEOUT_SECONDS) as response:
text = response.read().decode("utf-8").strip()
# Raises ValueError when the response is not a valid IPv4 address.
return str(ipaddress.IPv4Address(text))
def call_customer_api(method, path, body=None):
"""
Sends a request to the CyberGate Customer API and returns the JSON response (or None).
- method: "GET", "POST", "PATCH" or "DELETE"
- path: the part after the base URL, for example "/whitelist" or "/whitelist/12"
- body: optional dictionary, sent as JSON
Authentication: the API secret is sent as a Bearer token in the Authorization header.
On an error response, an exception is raised that includes the message returned by the API.
"""
headers = {
"Authorization": f"Bearer {API_SECRET}",
"Accept": "application/json",
"User-Agent": "CyberGate-IP-Updater",
}
data = None
if body is not None:
data = json.dumps(body).encode("utf-8")
headers["Content-Type"] = "application/json"
request = urllib.request.Request(API_BASE_URL + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(request, timeout=HTTP_TIMEOUT_SECONDS) as response:
content = response.read().decode("utf-8")
return json.loads(content) if content else None
except urllib.error.HTTPError as error:
details = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {path} failed with HTTP {error.code}: {details}") from error
def get_whitelist():
"""
GET /whitelist: returns all SIP white-list entries of your tenant.
Each entry looks like:
{"id": 12, "enabled": true, "ipAddressStart": "203.0.113.10", "ipAddressEnd": "203.0.113.10",
"description": "Main office", "location": "Amsterdam"}
"""
return call_customer_api("GET", "/whitelist")
def print_whitelist(whitelist):
"""Prints all white-list entries with their ID, to find the value for WHITELIST_ENTRY_ID."""
print(f"{'ID':>6} {'Enabled':<8} {'IP start':<16} {'IP end':<16} {'Description':<30} Location")
for entry in whitelist:
print(f"{entry['id']:>6} {entry['enabled']!s:<8} {entry['ipAddressStart']:<16} {entry['ipAddressEnd']:<16} "
f"{entry.get('description') or '':<30} {entry.get('location') or ''}")
def find_entry_by_id(whitelist, entry_id):
"""Returns the white-list entry with the given ID, or None when it does not exist."""
for entry in whitelist:
if entry["id"] == entry_id:
return entry
return None
def update_whitelist_entry(entry_id, ip_address):
"""
PATCH /whitelist/{id}: changes the IP address of an existing white-list entry.
A single IP address is a range where the start and end address are the same.
Only the properties in the request body are changed; description, location and the
enabled state of the entry are left as they are. Because the old address is replaced by the
new one in a single call, the number of white-listed addresses of your tenant stays the same.
"""
call_customer_api("PATCH", f"/whitelist/{entry_id}", {
"ipAddressStart": ip_address,
"ipAddressEnd": ip_address,
})
# ---------------------------------------------------------------------------------------------
# Main flow
# ---------------------------------------------------------------------------------------------
def main():
"""Checks the public IP address and updates the white-list entry when the address has changed."""
parser = argparse.ArgumentParser(
description="Keep the CyberGate SIP white-list in sync with a dynamic public IP address.")
parser.add_argument("--list", action="store_true", help="show all white-list entries with their ID and exit")
parser.add_argument("--dry-run", action="store_true", help="show what would change, without changing anything")
args = parser.parse_args()
# sys.exit() with a text prints that text as error and stops the script with exit code 1.
if not API_SECRET:
sys.exit("ERROR: environment variable CYBERGATE_API_SECRET is not set.")
# Read the white-list once; it is needed both for --list and for the update.
whitelist = get_whitelist()
if args.list:
print_whitelist(whitelist)
return
if WHITELIST_ENTRY_ID <= 0:
sys.exit("ERROR: WHITELIST_ENTRY_ID is not configured. Run this script with --list to find the ID.")
# Step 1: determine the current public IP address of this site.
current_ip = get_public_ip_address()
print(f"Current public IP address: {current_ip}")
# Step 2: look up the configured entry. It is never created by this script, only updated.
entry = find_entry_by_id(whitelist, WHITELIST_ENTRY_ID)
if entry is None:
sys.exit(f"ERROR: white-list entry {WHITELIST_ENTRY_ID} does not exist. "
"Run this script with --list to check the ID.")
# Safety check: this script manages a single IP address. When the entry is an IP range, the ID
# probably points to the wrong entry, and updating it would remove the rest of the range.
if entry["ipAddressStart"] != entry["ipAddressEnd"]:
sys.exit(f"ERROR: white-list entry {WHITELIST_ENTRY_ID} is an IP range "
f"({entry['ipAddressStart']} - {entry['ipAddressEnd']}), not a single IP address. "
"Check WHITELIST_ENTRY_ID.")
if not entry["enabled"]:
print("WARNING: this white-list entry is disabled in CyberGate, so the address is not allowed.")
old_ip = entry["ipAddressStart"]
# Step 3: the IP address has not changed, nothing to do.
if old_ip == current_ip:
print(f"IP address unchanged (entry {WHITELIST_ENTRY_ID}), nothing to do.")
return
# Step 4: the IP address has changed, replace the old address by the new one.
print(f"IP address changed from {old_ip} to {current_ip}, updating entry {WHITELIST_ENTRY_ID}.")
if not args.dry_run:
update_whitelist_entry(WHITELIST_ENTRY_ID, current_ip)
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, KeyError, RuntimeError) as error:
# OSError: network problems, such as no connection or a timeout.
# ValueError: unexpected response, such as no valid IP address or no valid JSON.
# KeyError: a field is missing in the response of the API.
# RuntimeError: the API returned an error, see call_customer_api().
# The error is reported on one line; exit code 1 lets a scheduler detect the failure.
sys.exit(f"ERROR: {error}")Running the script
The script reads the API secret from the environment variable CYBERGATE_API_SECRET, so the secret does not end up in the script file.
On Linux or macOS:
export CYBERGATE_API_SECRET="<your API secret>"
python3 update_dynamic_ip_whitelist.pyOn Windows (PowerShell):
$env:CYBERGATE_API_SECRET = "<your API secret>"
python update_dynamic_ip_whitelist.pyEach run prints one of these results:
Output | Meaning |
|---|---|
IP address unchanged (entry 12), nothing to do. | The white-list already contains the current address. |
IP address changed from 203.0.113.10 to 203.0.113.25, ... | The script replaced the old address with the new one. |
WARNING: this white-list entry is disabled in CyberGate ... | The entry exists but is switched off in the portal, so the address is not allowed. Enable it. |
ERROR: ... | Something went wrong. The line includes the reason, such as the response of the API. |
On an error the script stops with exit code 1, so a scheduler or monitoring tool can detect the failure.
Scheduling
Run the script every 5 minutes.
On Linux or macOS, use cron. Run crontab -e and add this line:
*/5 * * * * CYBERGATE_API_SECRET=<secret> python3 /opt/cybergate/update_dynamic_ip_whitelist.py >> /var/log/cybergate-ip.log 2>&1On Windows, create a task in Task Scheduler that runs python.exe with the full path of the script as argument, and repeat it every 5 minutes. Set CYBERGATE_API_SECRET as a system environment variable, so the task can read it without a logged-in user.
Do not run the script more often than every few minutes. The Customer API is rate limited per IP address and rejects calls above that limit.
Things to keep in mind
There is a short gap after each change. Between the moment the ISP changes the address and the next run of the script, the new address is not on the white-list yet. After the update, a device connects again at its next registration attempt. A shorter schedule makes the gap shorter, within the rate limit above.
The entry has to be a single IP address. The script refuses to update an entry that holds an IP range, because that usually means the configured ID points to the wrong entry. The API also rejects a new address that overlaps with another entry on your white-list.
The API secret expires. An API secret is valid for at most 365 days. Once it has expired, every run fails with an HTTP 401 error and the white-list is no longer updated. Create a new secret before the old one expires and update the environment variable on the computer at the site. The API Access page shows a warning icon when a secret is about to expire.
Changes appear in the Audit Trail. CyberGate logs each update through the API under the name of the API secret. Give the secret a clear name, for example Dynamic IP updater Utrecht, so you can see in the Audit Trail where a change came from.
Only IPv4 is supported. The CyberGate white-list holds IPv4 addresses, so the script asks the IP service for the IPv4 address of the site.