Skip to content
Notifications
Clear all

Step-by-step: Creating a read-only audit log feed for our SIEM (Splunk example)

3 Posts
3 Users
0 Reactions
0 Views
(@jackson2m)
Estimable Member
Joined: 1 week ago
Posts: 67
Topic starter   [#14793]

After implementing Twingate across our hybrid infrastructure to replace a legacy VPN, our security team’s immediate request was for a centralized, immutable audit trail. While Twingate’s Admin API provides comprehensive event data, we needed a reliable, automated feed into our existing Splunk deployment for correlation and long-term retention. The documentation covers the components, but I found the operational specifics for a production-ready, read-only pipeline required some assembly.

I'll outline the architecture we deployed, focusing on the "how" and the "why" behind each choice. This isn't a one-command setup; it's a systems integration project.

**Core Components & Rationale:**
* **Twingate Admin API:** The sole source of truth. We use it solely for `GET` operations to pull audit logs.
* **Dedicated Service Account:** A Twingate user with the "Observer" role. This provides the necessary read-only access globally across the entire Twingate network, adhering to the principle of least privilege.
* **Intermediate Log Buffer:** A lightweight container running a scheduled Python script. This is critical. It acts as a resilient buffer, handling API pagination, transient errors, and state management before forwarding to Splunk. Direct API-to-Splunk HTTP Event Collector (HEC) calls were deemed unreliable for our scale.
* **Splunk HTTP Event Collector (HEC):** The ingestion endpoint. We use a dedicated HEC token with a JSON source type.

**Key Implementation Details:**

The script in our buffer container performs the following logic in a continuous loop:
* **Stateful Bookmarking:** It persistently stores the `lastEventId` of the most recently processed event in a local file. Each subsequent API call uses the `after` parameter (e.g., ` https://api.twingate.com/api/v1/audit/logs?after=`) to fetch only new events. This is far more efficient than time-based polling and prevents gaps or duplicates.
* **Pagination Handling:** It iterates through all pages of the API response using the `pageInfo.endCursor` until `pageInfo.hasNextPage` is `false`, aggregating a complete set of new events.
* **Data Enrichment & Transformation:** The raw JSON event is wrapped into the required Splunk HEC JSON format. We add critical metadata fields at this stage, including the Twingate network ID and a deterministic event hash for de-duplication.
* **Batched Forwarding:** Events are queued and sent to Splunk HEC in batches of 100 to optimize throughput and avoid rate limiting.
* **Error Handling & Retry Logic:** Network failures or Splunk ingestion errors trigger an exponential backoff retry. The script will not advance the `lastEventId` bookmark unless Splunk acknowledges receipt, guaranteeing at-least-once delivery.

**Splunk Configuration Notes:**
We created a new index (`twingate_audit`) with a longer retention period. The HEC input is configured with the `json` source type, and we deployed a light props.conf transformation to ensure timestamp extraction from the Twingate `createdAt` field. This allows Splunk to use the event's original time, not the ingestion time.

**Pitfalls We Encountered & Resolved:**
* The Observer role is essential; a standard user account's token will not see all network events.
* The API's `first` parameter for limiting batch size is your friend. We set it to 1000 per page to balance performance and memory use in the container.
* Initially, we used time-window polling. This failed during high-event periods, as events can be written with slight timestamp drift, causing us to miss some. The cursor-based (`after`) method is fundamentally more robust.
* The audit log does not include all administrative actions performed via the Terraform provider. This is a known separation. Our complete governance picture requires a separate feed from our Terraform Cloud audit trail.

The resulting feed provides us with real-time visibility into user connections, resource access attempts, policy evaluations, and administrative changes. We've successfully correlated Twingate access denials with endpoint security events in Splunk, creating a much tighter zero-trust narrative for our auditors.


Data over opinions


   
Quote
(@backend_perf_guru)
Estimable Member
Joined: 5 months ago
Posts: 155
 

Your choice of an intermediate buffer is the key to a production system. The crucial metric you'll need to monitor is the delta between the latest timestamp you've ingested and the current time, as your script's execution interval plus any API latency or pagination overhead directly dictates your audit trail's freshness. A common pitfall I've seen is scripts that don't handle the HTTP 429 response correctly; you need exponential backoff with jitter, not just a fixed sleep.

Also, consider the serialization cost early. If you're writing NDJSON to disk in that buffer container before a forwarder picks it up, the filesystem I/O can become a bottleneck under high event volume. I'd recommend a quick benchmark comparing plain `json.dump()` versus `orjson` if you're using Python. The memory overhead difference is significant when you're processing thousands of events per poll.


--perf


   
ReplyQuote
(@jackt)
Trusted Member
Joined: 1 week ago
Posts: 40
 

You're dead right about the serialization cost. We saw the same I/O bottleneck in our initial POC. Swapping out the standard library json module for orjson cut our processing time for a 10k event batch by about 40%, and the memory profile was noticeably flatter. The kicker? The orjson package uses Rust under the hood, so you're getting that performance without introducing a complex external service.

The 429 handling is another place teams get burned. A fixed sleep just pushes the problem down the road and can cause your consumer to fall hopelessly behind. Exponential backoff with jitter is non-negotiable. I'd add that you need to make sure your retry logic respects the `Retry-After` header if the API provides one, otherwise you're just guessing.


been there, migrated that


   
ReplyQuote