XCloak / blog
← All posts
2026-07-07 · Release · 6 min read

XCloak v0.2.0: Enterprise Security Hardening

v0.2.0 is the most significant XCloak release to date. The focus was depth over breadth: hardening everything already built rather than adding more features.

Here's what changed.

Backend Security Hardening

httpOnly Cookie Auth and Refresh Token Rotation

Access tokens were previously returned only in the JSON response body and stored by the frontend in memory. This works but leaves a gap: there's no robust mechanism for token renewal without user re-login.

v0.2 introduces httpOnly cookie-based session management. On every login, the backend issues:

The refresh token is rotated on every use — each call to /api/auth/refresh invalidates the old token and issues a new one. Old tokens are revoked in Redis. This protects against token theft even if a refresh token is somehow extracted.

Mobile clients (Dart/Android) additionally receive tokens in the response body because they don't use browser cookies.

Closing the Rate Limiter TOCTOU Race

The previous sliding-window rate limiter had a subtle race condition: it read the window count, then wrote the new entry in two separate Redis commands. Under concurrent requests, two requests could both pass the limit check before either incremented the counter.

The fix is a single atomic Lua script executed via Redis EVAL:

local key    = KEYS[1]
local now    = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit  = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
local count = redis.call('ZCARD', key)
if count >= limit then
  return 0
end
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('EXPIRE', key, math.ceil(window / 1000))
return 1

ZREMRANGEBYSCORE, ZCARD, and ZADD happen atomically. No gap between read and write. This closes the race entirely. Full write-up: Closing the TOCTOU Gap in Redis Rate Limiting.

PostgreSQL RLS Operationally Active

XCloak had Row-Level Security policies defined since day one, but the app was connecting to PostgreSQL as a superuser — which bypasses RLS. The policies existed on paper but didn't enforce anything.

Migration 000057 creates the xcloak_app role (DML-only: SELECT, INSERT, UPDATE, DELETE on all tables, no DDL) and grants it default privileges for future migrations. The backend now uses a separate pool that connects as xcloak_app. The migration pool (DDL, runs on startup) still connects as the superuser.

With this change, RLS is load-bearing for every query. A bug in the application that forgets to set app.tenant_id returns no rows rather than all rows.

Kafka Event Bus Wired End-to-End

Five publish functions existed in the codebase and were never called. v0.2 wires them:

Six consumer goroutines process these events: FIM auto-quarantine, YARA auto-quarantine, incident webhook delivery, audit Splunk streaming, WebSocket broadcast, IOC correlation.

FIM and YARA matches now automatically create quarantine_file tasks in the pending-approval queue. Operators review and approve from the Tasks screen; the agent only acts after explicit approval.

Go Agent Enterprise Upgrade

Process-Aware Network Connections

The previous agent reported raw socket tuples: src_ip:port → dst_ip:port with no process context. This meant an analyst had to manually correlate network data with process data from a separate tab.

v0.2 maps every connection to its owning process using the Linux /proc filesystem:

/proc/net/tcp   → hex-encoded socket table with inode numbers
/proc/<pid>/fd/ → symlinks like "socket:[inode]" for each open fd
/proc/<pid>/comm → process name
/proc/<pid>/exe → executable path (symlink)

The agent builds a reverse map: inode → PID → name/path, then enriches every TCP/UDP entry. The result: every connection in the UI shows the owning process name, PID, and exe path without any additional queries. Full write-up: How XCloak Maps Linux /proc/net to Process Names.

On Windows, the agent uses netstat -ano (which includes PID) and tasklist /fo csv (which maps PID → name), combined via a lookup table.

Full User Inventory

The previous user collector gathered: username, UID, shell. v0.2 expands this to 12 fields per user:

type User struct {
  Username    string
  UID         int
  GID         int
  HomeDir     string
  Shell       string
  Groups      []string  // from /etc/group
  SudoAccess  bool      // from /etc/sudoers + wheel/sudo/admin group
  HasSSHKey   bool      // ~/.ssh/authorized_keys exists and non-empty
  LastLogin   string    // from `last -n 1 <username>`
  Enabled     bool      // /etc/shadow password field (!/* = locked)
}

Sudoers parsing covers /etc/sudoers, /etc/sudoers.d/*, and group membership checks (wheel, sudo, admin groups). This gives SOC analysts an accurate picture of lateral movement paths and privilege escalation vectors.

Android Mobile Agent Enterprise Upgrade

24 Posture Fields

The previous posture check collected 8 fields. v0.2 expands to 24:

Retry with Exponential Backoff

The previous ApiClient had no retry logic. Any transient network hiccup caused a check-in to fail silently. On a mobile device moving between WiFi and mobile networks, this was a frequent occurrence.

v0.2 adds backoff inside ApiClient._withRetry():

Thundering Herd Prevention

If 500 devices all reboot simultaneously (e.g. after an OS update push), they would all start their background timers at the same time and hit the backend in a synchronized burst.

v0.2 adds _schedulePeriodic(), which applies random jitter (0–30 s) before the first tick of each timer. The second and subsequent ticks run on the fixed interval. On a 500-device fleet, check-ins are spread across 30 seconds instead of firing simultaneously. Full write-up: Building Enterprise Android MDM Without Google EMM.

← Building Enterprise Android MDM Without Google EMM How a log line becomes an alert →