Collect LenelS2 OnGuard logs
This document explains how to collect logs from LenelS2 (Honeywell) OnGuard and forward them to Google Security Operations using a webhook feed. Because OnGuard does not natively support direct webhook or HTTP POST delivery, this guide uses a custom middleware script that polls the OnGuard OpenAccess REST API for events and forwards them to the Google SecOps webhook ingestion endpoint.
LenelS2 OnGuard is a physical access control and badge management system that manages cardholders, credentials, readers, panels, and access events across enterprise facilities. The OpenAccess REST API provides programmatic access to OnGuard data including logged events such as access granted, access denied, door held open, door forced open, and alarm events.
Before you begin
Make sure you have the following prerequisites:
- A Google SecOps instance
- An OnGuard server (version 7.5 or later) with the OpenAccess license activated
The following OnGuard services running on the OpenAccess server:
- LS Communication Server
- LS Event Context Provider
- LS Web Event Bridge
- LS OpenAccess
- LS Web Service
An internal OnGuard user account with the following minimum permissions:
- System Permissions: Under Access control hardware, Query permission on Access Panels, Readers, and Alarm Panels
- System Permissions: Under Access control, Query permission on Segments
- Monitor Permissions: Under Control, Open doors and Relay and reader outputs
Network connectivity from the middleware host to the OnGuard server on TCP port
8080(default OpenAccess port)Network connectivity from the middleware host to the internet (to reach the Google SecOps webhook endpoint)
Python 3.8 or later installed on the middleware host
Access to Google Cloud Console (for API key creation)
Create webhook feed in Google SecOps
Create the feed
- Go to SIEM Settings > Feeds.
- Click Add New Feed.
- On the next page, click Configure a single feed.
- In the Feed name field, enter a name for the feed (for example,
LenelS2 OnGuard Events). - Select Webhook as the Source type.
- Select Lenel OnGuard as the Log type.
- Click Next.
- Specify values for the following input parameters:
- Split delimiter: Enter
\n(newline delimiter, because the middleware sends events in NDJSON format) - Asset namespace: The asset namespace
- Ingestion labels: The label to be applied to the events from this feed
- Split delimiter: Enter
- Click Next.
- Review your new feed configuration in the Finalize screen, and then click Submit.
Generate and save secret key
After creating the feed, you must generate a secret key for authentication:
- On the feed details page, click Generate Secret Key.
- A dialog displays the secret key.
- Copy and save the secret key securely.
Get the feed endpoint URL
- Go to the Details tab of the feed.
- In the Endpoint Information section, copy the Feed endpoint URL.
The URL format is:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreateor
https://<REGION>-malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreateSave this URL for the next steps.
Click Done.
Create Google Cloud API key
Chronicle requires an API key for authentication. Create a restricted API key in the Google Cloud Console.
Create the API key
- Go to the Google Cloud Console Credentials page.
- Select your project (the project associated with your Chronicle instance).
- Click Create credentials > API key.
- An API key is created and displayed in a dialog.
- Click Edit API key to restrict the key.
Restrict the API key
- In the API key settings page:
- Name: Enter a descriptive name (for example,
Chronicle Webhook API Key)
- Name: Enter a descriptive name (for example,
- Under API restrictions:
- Select Restrict key.
- In the Select APIs dropdown, search for and select Google SecOps API (or Chronicle API).
- Click Save.
- Copy the API key value from the API key field at the top of the page.
- Save the API key securely.
Verify OnGuard OpenAccess prerequisites
Before configuring the middleware, verify that the OnGuard OpenAccess API is operational.
Verify the OpenAccess service is running
- On the OnGuard server, open Windows Services (
services.msc). - Confirm that the following services are running:
- LS OpenAccess
- LS Communication Server
- LS Event Context Provider
- LS Web Event Bridge
- LS Web Service
- LS Message Broker (RabbitMQ)
- If any service is stopped, right-click the service and select Start.
Verify the OpenAccess API is accessible
- Open a web browser on the middleware host.
Navigate to the following URL:
https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess/version?version=1.0Replace
<ONGUARD_SERVER>with the IP address or hostname of your OnGuard server.The API returns a JSON response containing the OpenAccess version details, confirming the service is accessible.
Retrieve the authentication directory
Send a GET request to retrieve the available authentication directories:
https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess/directories?version=1.0The response contains a list of directories. Note the
IDandNameof the directory you will use for authentication (typically the internal OnGuard directory).
Verify the OnGuard user account
- Open the OnGuard System Administration application on the OnGuard server.
- Navigate to Administration > Users.
- Select the user account designated for API access.
- Click the Permissions tab.
- Under System Permission Group, confirm the following permissions are enabled:
- Under Access control hardware: Query on Access Panels, Readers, and Alarm Panels
- Under Access control: Query on Segments
- Under Monitor Permission Group, confirm the following permissions are enabled:
- Under Control: Open doors
- Under Control: Relay and reader outputs
Obtain an Application ID
Each application using the OpenAccess API must have a unique Application ID. This is a string value you define (for example, ChronicleForwarder). The Application ID is sent as the Application-Id HTTP header with every API request.
Configure the OnGuard-to-Chronicle middleware
Because OnGuard does not natively support outbound webhooks or HTTP POST delivery, a middleware script is required to poll the OpenAccess REST API for logged events and forward them to the Google SecOps webhook endpoint.
Construct the Chronicle webhook URL
Combine the Chronicle endpoint URL and API key:
<ENDPOINT_URL>?key=<API_KEY>Example:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...
Create the middleware script
On the middleware host, create a directory for the script:
mkdir -p /opt/onguard-chronicle-forwarder cd /opt/onguard-chronicle-forwarderInstall the required Python package:
pip3 install requestsCreate the forwarder script:
nano onguard_to_chronicle.pyAdd the following content to the script:
#!/usr/bin/env python3 """LenelS2 OnGuard to Google SecOps (Chronicle) Event Forwarder. Polls the OnGuard OpenAccess REST API for logged events and forwards them to a Chronicle webhook ingestion endpoint. """ import json import logging import os import sys import time from datetime import datetime, timedelta, timezone import requests import urllib3 # Suppress SSL warnings if using self-signed certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # ── Configuration ────────────────────────────────────────────────── ONGUARD_HOST = os.environ.get("ONGUARD_HOST", "192.168.1.100") ONGUARD_PORT = os.environ.get("ONGUARD_PORT", "8080") ONGUARD_USERNAME = os.environ.get("ONGUARD_USERNAME", "") ONGUARD_PASSWORD = os.environ.get("ONGUARD_PASSWORD", "") ONGUARD_DIRECTORY_ID = os.environ.get("ONGUARD_DIRECTORY_ID", "") APPLICATION_ID = os.environ.get("APPLICATION_ID", "ChronicleForwarder") VERIFY_SSL = os.environ.get("VERIFY_SSL", "false").lower() == "true" CHRONICLE_ENDPOINT = os.environ.get("CHRONICLE_ENDPOINT", "") CHRONICLE_API_KEY = os.environ.get("CHRONICLE_API_KEY", "") CHRONICLE_SECRET_KEY = os.environ.get("CHRONICLE_SECRET_KEY", "") POLL_INTERVAL_SECONDS = int(os.environ.get("POLL_INTERVAL_SECONDS", "30")) PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "100")) LOOKBACK_MINUTES = int(os.environ.get("LOOKBACK_MINUTES", "5")) # ── Logging ──────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler("/opt/onguard-chronicle-forwarder/forwarder.log"), ], ) logger = logging.getLogger(__name__) BASE_URL = f"https://{ONGUARD_HOST}:{ONGUARD_PORT}/api/access/onguard/openaccess" class OnGuardClient: """Client for the OnGuard OpenAccess REST API.""" def __init__(self): self.session_token = None self.headers = { "Content-Type": "application/json", "Application-Id": APPLICATION_ID, } def authenticate(self): """Authenticate to OpenAccess and obtain a session token.""" url = f"{BASE_URL}/authentication?version=1.0" payload = { "user_name": ONGUARD_USERNAME, "password": ONGUARD_PASSWORD, "directory_id": ONGUARD_DIRECTORY_ID, } try: resp = requests.post( url, headers=self.headers, json=payload, verify=VERIFY_SSL, timeout=30, ) resp.raise_for_status() data = resp.json() self.session_token = data.get("session_token") self.headers["Session-Token"] = self.session_token logger.info("Successfully authenticated to OnGuard OpenAccess.") except requests.exceptions.RequestException as exc: logger.error("Authentication failed: %s", exc) raise def keepalive(self): """Renew the session idle timeout.""" url = f"{BASE_URL}/keepalive?version=1.0" try: resp = requests.get( url, headers=self.headers, verify=VERIFY_SSL, timeout=15 ) resp.raise_for_status() except requests.exceptions.RequestException: logger.warning("Keepalive failed. Re-authenticating.") self.authenticate() def get_logged_events(self, start_time, page_number=1): """Retrieve a page of logged events since start_time.""" url = f"{BASE_URL}/logged_events?version=1.0" params = { "filter": f"timestamp >= '{start_time}'", "page_number": page_number, "page_size": PAGE_SIZE, "order_by": "timestamp", } try: resp = requests.get( url, headers=self.headers, params=params, verify=VERIFY_SSL, timeout=30, ) resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as exc: logger.error("Failed to retrieve logged events: %s", exc) return None def logout(self): """Logout and invalidate the session token.""" url = f"{BASE_URL}/authentication?version=1.0" try: requests.delete( url, headers=self.headers, verify=VERIFY_SSL, timeout=15 ) logger.info("Logged out of OnGuard OpenAccess.") except requests.exceptions.RequestException: pass def send_to_chronicle(events): """Send a batch of events to the Chronicle webhook endpoint.""" if not events: return True url = f"{CHRONICLE_ENDPOINT}?key={CHRONICLE_API_KEY}" headers = { "Content-Type": "application/json", "x-chronicle-auth": CHRONICLE_SECRET_KEY, } # Send events as NDJSON (newline-delimited JSON) body = "\n".join(json.dumps(event) for event in events) try: resp = requests.post(url, headers=headers, data=body, timeout=30) resp.raise_for_status() logger.info("Sent %d events to Chronicle.", len(events)) return True except requests.exceptions.RequestException as exc: logger.error("Failed to send events to Chronicle: %s", exc) return False def main(): """Main polling loop.""" logger.info("Starting OnGuard-to-Chronicle forwarder.") client = OnGuardClient() client.authenticate() last_poll_time = datetime.now(timezone.utc) - timedelta(minutes=LOOKBACK_MINUTES) while True: try: current_time = datetime.now(timezone.utc) start_time_str = last_poll_time.strftime("%Y-%m-%dT%H:%M:%SZ") all_events = [] page = 1 while True: result = client.get_logged_events(start_time_str, page_number=page) if result is None: # Session may have expired; re-authenticate client.authenticate() result = client.get_logged_events( start_time_str, page_number=page ) if result is None: break events = result.get("logged_events", []) if not events: break all_events.extend(events) total = result.get("total_items", 0) if page * PAGE_SIZE >= total: break page += 1 if all_events: success = send_to_chronicle(all_events) if success: last_poll_time = current_time else: last_poll_time = current_time logger.debug("No new events found.") # Send keepalive to maintain session client.keepalive() except KeyboardInterrupt: logger.info("Shutting down.") client.logout() sys.exit(0) except Exception as exc: logger.error("Unexpected error: %s", exc) try: client.authenticate() except Exception: pass time.sleep(POLL_INTERVAL_SECONDS) if __name__ == "__main__": main()Save and close the file.
Configure environment variables
Create an environment file for the forwarder:
nano /opt/onguard-chronicle-forwarder/.envAdd the following configuration values:
ONGUARD_HOST=<ONGUARD_SERVER_IP_OR_HOSTNAME> ONGUARD_PORT=8080 ONGUARD_USERNAME=<ONGUARD_API_USERNAME> ONGUARD_PASSWORD=<ONGUARD_API_PASSWORD> ONGUARD_DIRECTORY_ID=<DIRECTORY_ID> APPLICATION_ID=ChronicleForwarder VERIFY_SSL=false CHRONICLE_ENDPOINT=<CHRONICLE_WEBHOOK_ENDPOINT_URL> CHRONICLE_API_KEY=<GOOGLE_CLOUD_API_KEY> CHRONICLE_SECRET_KEY=<CHRONICLE_FEED_SECRET_KEY> POLL_INTERVAL_SECONDS=30 PAGE_SIZE=100 LOOKBACK_MINUTES=5- ONGUARD_HOST: The IP address or hostname of the OnGuard server running the OpenAccess service (for example,
192.168.1.100) - ONGUARD_PORT: The port the OpenAccess service listens on (default:
8080) - ONGUARD_USERNAME: The OnGuard internal user account username
- ONGUARD_PASSWORD: The password for the OnGuard user account
- ONGUARD_DIRECTORY_ID: The directory ID retrieved from the
GET /directoriesendpoint - APPLICATION_ID: A unique identifier for this integration (for example,
ChronicleForwarder) - VERIFY_SSL: Set to
trueif the OnGuard server uses a trusted SSL certificate, orfalsefor self-signed certificates - CHRONICLE_ENDPOINT: The webhook endpoint URL from the Chronicle feed configuration
- CHRONICLE_API_KEY: The Google Cloud API key created earlier
- CHRONICLE_SECRET_KEY: The secret key generated during Chronicle feed creation
- POLL_INTERVAL_SECONDS: How frequently (in seconds) to poll for new events (default:
30) - PAGE_SIZE: Number of events to retrieve per API page (default:
100, maximum:1000) - LOOKBACK_MINUTES: On initial startup, how many minutes of historical events to retrieve (default:
5)
- ONGUARD_HOST: The IP address or hostname of the OnGuard server running the OpenAccess service (for example,
Secure the environment file:
chmod 600 /opt/onguard-chronicle-forwarder/.env
Install as a systemd service
Create a systemd service file:
sudo nano /etc/systemd/system/onguard-chronicle-forwarder.serviceAdd the following content:
[Unit] Description=LenelS2 OnGuard to Chronicle Event Forwarder After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/onguard-chronicle-forwarder EnvironmentFile=/opt/onguard-chronicle-forwarder/.env ExecStart=/usr/bin/python3 /opt/onguard-chronicle-forwarder/onguard_to_chronicle.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.targetEnable and start the service:
sudo systemctl daemon-reload sudo systemctl enable onguard-chronicle-forwarder.service sudo systemctl start onguard-chronicle-forwarder.serviceVerify the service is running:
sudo systemctl status onguard-chronicle-forwarder.serviceCheck the forwarder log for successful operation:
tail -f /opt/onguard-chronicle-forwarder/forwarder.logA successful startup shows:
Starting OnGuard-to-Chronicle forwarder. Successfully authenticated to OnGuard OpenAccess. Sent <N> events to Chronicle.
OpenAccess API reference
The middleware uses the following OnGuard OpenAccess REST API endpoints. All endpoints are relative to https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess.
Authentication
| Method | Endpoint | Description |
|---|---|---|
POST |
/authentication?version=1.0 |
Login and retrieve a session token |
DELETE |
/authentication?version=1.0 |
Logout and invalidate the session token |
GET |
/directories?version=1.0 |
Get available authentication directories |
GET |
/keepalive?version=1.0 |
Renew the session idle timeout |
Required HTTP headers for all authenticated requests:
| Header | Description |
|---|---|
Application-Id |
Unique application identifier string (for example, ChronicleForwarder) |
Session-Token |
Session token returned by POST /authentication |
Content-Type |
application/json |
Events
| Method | Endpoint | Description |
|---|---|---|
GET |
/logged_events?version=1.0 |
Retrieve a page of logged events |
POST |
/event_subscriptions?version=1.0 |
Create a real-time event subscription |
GET |
/event_subscriptions?version=1.0 |
List event subscriptions |
Event types
OnGuard generates the following categories of events that are forwarded to Google SecOps:
- Access Granted Events: Badge swipe accepted at a reader
- Access Denied Events: Badge swipe rejected (invalid badge, expired, wrong access level)
- Door Held Open Events: Door remains open beyond the configured time
- Door Forced Open Events: Door opened without a valid badge swipe
- Alarm Events: Alarm input triggered or acknowledged
- Status Events: Panel online/offline, communication failure, tamper
- Cardholder Events: Cardholder added, modified, or deleted
- Badge Events: Badge activated, deactivated, or lost
- Visitor Events: Visitor signed in or signed out
Authentication methods reference
Chronicle webhook feeds support multiple authentication methods. Choose the method that your vendor supports.
Method 1: Custom headers (Recommended)
If your vendor supports custom HTTP headers, use this method for better security.
Request format:
POST <ENDPOINT_URL> HTTP/1.1 Content-Type: application/json x-goog-chronicle-auth: <API_KEY> x-chronicle-auth: <SECRET_KEY> { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
Advantages: - API key and secret not visible in URL - More secure (headers not logged in web server access logs) - Preferred method when vendor supports it
Method 2: Query parameters
If your vendor does not support custom headers, append credentials to the URL.
URL format:
<ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY>Example:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...&secret=abcd1234...Request format:
POST <ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY> HTTP/1.1 Content-Type: application/json { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
Disadvantages:
- Credentials visible in URL
- May be logged in web server access logs
- Less secure than headers
Method 3: Hybrid (URL + Header)
Some configurations use API key in URL and secret key in header.
Request format:
POST <ENDPOINT_URL>?key=<API_KEY> HTTP/1.1 Content-Type: application/json x-chronicle-auth: <SECRET_KEY> { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
Authentication header names
Chronicle accepts the following header names for authentication:
For API key:
x-goog-chronicle-auth(recommended)X-Goog-Chronicle-Auth(case-insensitive)
For secret key:
x-chronicle-auth(recommended)X-Chronicle-Auth(case-insensitive)
Troubleshooting
OpenAccess API connection failures
If the middleware cannot connect to the OpenAccess API:
Verify the OnGuard server is reachable from the middleware host:
curl -k https://<ONGUARD_SERVER>:8080/api/access/onguard/openaccess/version?version=1.0Confirm that port
8080is open on the OnGuard server firewall.On the OnGuard server, run the following command to verify the port is listening:
netstat -anb | findstr 8080Restart the LS OpenAccess service if the port is not listening:
- Open Windows Services (
services.msc) on the OnGuard server. - Right-click LS OpenAccess and select Restart.
- Open Windows Services (
Authentication failures
If the middleware receives authentication errors:
- Verify the OnGuard username and password are correct.
- Confirm the
ONGUARD_DIRECTORY_IDmatches a valid directory returned byGET /directories. - Ensure the OnGuard user account is not locked out due to brute force protection. OnGuard locks accounts after multiple failed login attempts.
- If the
openaccess.inifile exists atC:\ProgramData\lnl\openaccess.ini, verify its configuration is correct.
No events received in Chronicle
If the middleware runs without errors but no events appear in Chronicle:
- Verify that events are being generated in OnGuard by checking the Alarm Monitoring application.
Confirm that event publishing is enabled in the OnGuard database. On the OnGuard database server, run the following SQL query:
SELECT * FROM LNL_SYSTEMSETTINGS WHERE SETTINGNAME = 'EventPublishingDisabled'- A result of
0or no results means event publishing is enabled. - A result of
1means event publishing is disabled and must be re-enabled.
- A result of
Check the forwarder log file at
/opt/onguard-chronicle-forwarder/forwarder.logfor error messages.Verify the Chronicle webhook endpoint URL, API key, and secret key are correct in the
.envfile.
Request pool full errors
If the middleware receives request pool full errors from OpenAccess:
- On the OnGuard server, create or edit the file
C:\ProgramData\Lnl\OpenAccess.ini. Add the following content:
[http_request] request_pool_size=128Restart the LS OpenAccess service for the change to take effect.
Webhook limits and best practices
Request limits
| Limit | Value |
|---|---|
| Max request size | 4 MB |
| Max QPS (queries per second) | 15,000 |
| Request timeout | 30 seconds |
| Retry behavior | Automatic with exponential backoff |
UDM mapping table
| Original log field | UDM mapping | Logic |
|---|---|---|
timestamp |
metadata.event_timestamp |
Mapped directly from the event timestamp |
description |
metadata.description |
Event description text |
event_type |
metadata.product_event_type |
OnGuard event type identifier |
badge_id |
principal.user.product_object_id |
Badge number associated with the event |
cardholder_first_name |
principal.user.first_name |
First name of the cardholder |
cardholder_last_name |
principal.user.last_name |
Last name of the cardholder |
device |
principal.asset.hostname |
Reader or panel name |
source |
target.asset.hostname |
Source device or panel |
serial_number |
principal.asset.hardware.serial_number |
Panel serial number |
access_result |
security_result.action |
ALLOW or BLOCK based on access granted or denied |
Need more help? Get answers from Community members and Google SecOps professionals.