Skip to content

Identity

Identity is what turns 192.168.10.42 called api.openai.com into dsmith (group: engineering) called api.openai.com. It's what lets a rule read allow group:engineering → api.anthropic.com instead of a list of IPs, and what makes the audit trail name people. For the model, see Identity model.

Where you do this: Three surfaces, each with its own callout below.

  • User / group attribution → cloud dashboard, Identity page. Requires tenant_admin.
  • Workload identity (eBPF host sensor) → runs on each agent host — Kubernetes DaemonSet, or a systemd unit on a VM.
  • In-agent identity (kilasec_agent Python package) → installed inside the agent process or its container.

How attribution works

The collector resolves the source IP of each request to a user and their directory groups, using a mapping you feed it. With no identity source wired, requests attribute to the IP (and agent), which still works — you just don't get user/group rules or names.

Sources

You can populate the IP → user → groups map from any of these:

  • Active Directory (User-ID) — the live source. Reads domain-controller logon events and maps each one to IP → user. Two ways to get those events, below.
  • Directory sync (LDAP) — the other half of AD: who exists and what groups they're in. It cannot produce IP → user on its own — LDAP is a phone book, not a session table — so pair it with User-ID.
  • DHCP — map leases to users where your DHCP data carries that.
  • CSV import — a static ip,user,groups upload for fixed assignments (servers, kiosks, lab machines). The simplest way to start.

In the dashboard → Identity. The CSV importer and the manual override live there. For the API, see Identities and DHCP leases.

User-ID: mapping IP → user from Active Directory

Only the domain controller knows that 10.0.3.42 is alice right now — that fact lives in the Security log at the moment she authenticates, and nothing on the network can infer it. The User-ID connector turns those logon events (Security event 4624) into identity mappings with a TTL that expires like a logon session.

There are two ways to get the events to it. Neither installs software on a domain controller.

Agentless (pull)Syslog (push)
What you set upone read-only service accounta forwarder (WEF/NXLog/Snare)
Runs on the DCnothingnothing (WEF) / an agent (NXLog)
Latencypoll interval, default 30snear-instant
Where it runsany Linux/Windows box that can reach the DCsthe collector, built in
Extra dependencypip install pywinrmnone

Start with agentless — "create a service account" is a smaller ask than "install a forwarder on every domain controller". Move to syslog if you already run WEF, or if you need sub-second attribution.

Agentless: what your AD admin does

Nothing is installed and no reboot is needed. Most of this is verification on a modern DC.

  1. Create a domain service account — e.g. svc-kilasec-userid. Read-only; no interactive logon required.
  2. Add it to two built-in groups. On domain controllers these are domain-local groups in the Builtin container, so you add the account once and it covers every DC:
    • Event Log Readers — read the Security log
    • Remote Management Users — connect over WinRM without being an admin
  3. Confirm WinRM is listening. On by default since Server 2012, and usually enabled domain-wide by GPO:
    powershell
    Test-WSMan dc01.acme.local              # from the box that will poll
    Enable-PSRemoting -Force                # only if it isn't
  4. Confirm logon auditing is on — the default for DCs via Default Domain Controllers Policy:
    auditpol /get /subcategory:"Logon"
  5. Allow inbound TCP 5985 (or 5986 for HTTPS) on each DC, scoped to the polling host's IP. The Windows Remote Management (HTTP-In) firewall rule usually exists already — you are narrowing its scope, not creating it.

Get-WinEvent runs on the DC as your service account, which is why Event Log Readers is enough. That is also why this is far less work than a WMI-based agentless mode, which needs DCOM, a dynamic RPC port range through the firewall, and a registry SDDL edit to grant remote Security-log reads.

If your security team objects to handing over a password, a JEA endpoint exposing only Get-WinEvent limits the account to exactly this one operation.

Agentless: running the connector

Unpack the connector on any host that can reach both your DCs and Kilasec — the collector host is a fine choice — then:

bash
pip install pywinrm

export KILASEC_CLOUD_URL=https://kilasec.com
export KILASEC_TOKEN=agt_…                       # tenant API token
export USERID_WINRM_DCS=dc01.acme.local,dc02.acme.local
export USERID_WINRM_USER='ACME\svc-kilasec-userid'
export USERID_WINRM_PASSWORD='…'
export USERID_DOMAIN_STRIP=@acme.com             # optional: strip a suffix your
                                                 # forwarder glued on

python3 connector.py

The credential never leaves your network. Kilasec does not store it and does not ask for it — the account can read every logon in the domain, so it stays on the box you run this on.

Every DC that authenticates users needs to be in USERID_WINRM_DCS: a logon only lands on the DC that served it, so a partial list means partial coverage.

Useful knobs:

VariableDefault
USERID_POLL_SECS30how often each DC is polled
USERID_TTL_SECONDS28800mapping lifetime — match your logon session
USERID_WINRM_TRANSPORTntlmor kerberos, credssp
USERID_WINRM_HTTPS01 to use a 5986 HTTPS listener
USERID_WINRM_VERIFY10 to accept a self-signed listener cert
USERID_WINRM_MAX_EVENTS5000per-poll cap, so a busy DC can't be hammered

On plain HTTP (5985), NTLM and Kerberos both encrypt the payload at the message layer, so the logon data is not on the wire in the clear. Keep WinRM Basic auth disabled.

Syslog: forwarding events instead

Point any forwarder at the collector's UDP 5514 — the connector is built into the collector image and listens there by default. Filter to event ID 4624 and send JSON:

<Extension json>
    Module      xm_json
</Extension>
<Input eventlog>
    Module      im_msvistalog
    <QueryXML>
      <QueryList><Query Id="0">
        <Select Path="Security">*[System[(EventID=4624)]]</Select>
      </Query></QueryList>
    </QueryXML>
</Input>
<Output kilasec>
    Module      om_udp
    Host        <collector-ip>
    Port        5514
    Exec        to_json();
</Output>
<Route r>
    Path        eventlog => kilasec
</Route>

Send JSON, not the raw message. Windows' descriptive 4624 text does not contain the string 4624 anywhere in its body — the event ID is metadata — and the connector ignores anything that doesn't. to_json() includes EventID, so it matches. JSON is also compact enough to survive forwarders that truncate UDP syslog at 1024 bytes, which would otherwise chop off Source Network Address and yield zero mappings.

Verifying either mode

  • The connector logs 10.0.3.42 -> alice@acme.local per logon and pushed N mapping(s) per flush.
  • The dashboard's Identity page shows the rows, with source ad-agentless or ad-userid so you can tell which path produced them.
  • Machine accounts (…$), ANONYMOUS LOGON, service accounts, and loopback/empty source IPs are skipped — they aren't people. So is the User-ID service account itself: every poll signs it in to the DC, and that logon lands in the log we're reading.

How users are named

Mappings are domain-qualified: alice@acme.local, not alice. A bare sAMAccountName isn't an identity — every domain in a forest can have its own alice, so a rule written against the short name would match all of them.

Event 4624 reports the domain in NetBIOS form (ACME), which is not a UPN suffix. Agentless mode learns the DNS domain from the DC it polls and builds alice@acme.local from it. Syslog mode has no DC to ask — set USERID_DNS_DOMAIN=acme.local to get UPNs there. When the suffix can't be determined (a logon from a different domain in the forest, say), the mapping stays ACME\alice rather than inventing a domain it can't verify.

In policy, user: alice still matches alice@acme.local, so rules written before this keep working. Write the qualified form when two domains share a username and you mean one of them:

yaml
match:
  user: alice@acme.local      # exact
  user: acme\alice            # NetBIOS spelling, same person
  user: alice                 # any domain's alice — fine until there are two
  • Failures name the fix rather than the stack: access denied reading the Security log — add the account to Event Log Readers.

Getting started

  1. Start with a CSV for the machines you already know (servers, shared boxes, a pilot group). Immediate value, no integration.
  2. Wire a live source once you want dynamic, fleet-wide attribution that follows users: connect the directory for groups, then User-ID for IP → user. Both are needed before group: rules fire on real traffic.
  3. Write group rules once attribution is flowing — now group: and user: conditions in policy resolve.

Why it matters for policy and audit

  • Policy gets expressive and stable: rules keyed on groups survive DHCP churn and re-imaging, where IP-based rules rot.
  • Audit gets accountable: the Traffic and Audit logs name the person, which is what an incident responder and an auditor actually need.

Workload identity — co-located agents (eBPF host sensor)

Where you deploy this: On each agent host (the VM/node running the agents), as root. Kubernetes: a DaemonSet from the agents bundle. VMs: a systemd unit. Deploy recipes live in kilasec-agents/hostsensor/deploy/ — see the README there for the four-command Kubernetes walkthrough.

IP → user attribution breaks down when many agents share one source IP — a VM or pod running hundreds of AI agents all egress from the same address. For that, Kilasec adds a per-flow layer: the host sensor, a small agent that uses eBPF — via Tetragon — to record which process/container opened each outbound connection.

The sensor pushes (source IP, source port) → {workload, container, pod, labels} to the cloud with a short TTL. The collector records each request's source port as well as its IP, so the who-is lookup joins the flow to the exact process behind it — kernel-observed, no app changes, and not spoofable the way a User-Agent or self-reported agent name is.

What you get once the sensor is running:

  • In the dashboard → Live Traffic shows the owning workload (process/pod) under the source IP for flows evaluated against workload rules.
  • In the dashboard → Policy Rules → Rule editor → Source tab you can now match on workload: (the process binary's full path, or the pod / container name) and workload_labels: (key=value Kubernetes pod labels, ANY-of).

The binary is matched by full path on purpose: /usr/local/bin/agentd and a rogue /tmp/agentd must not satisfy the same rule. In Kubernetes, prefer matching the pod or container name — those are the stable handles.

yaml
- name: payments_bot_only_anthropic
  match:
    workload: [payments-7f9]          # pod name; or /usr/local/bin/payments-bot
    destination:
      host: ["*.anthropic.com"]
  action: allow
- name: prod_workloads_no_shadow_ai
  match:
    workload_labels: [env=prod]
    destination_is_unknown_ai: true
  action: deny

Fail-closed, and what that means for "unattributed" traffic. A flow with no attribution never matches a workload rule — the rule is skipped, and the flow falls through to the rules below it. A workload rule therefore cannot be used to catch unattributed traffic. To constrain that, put a catch-all rule (no workload predicate) after your workload allows, e.g. destination_is_ai_provider: true → require_approval — attributed-and-allowed flows match above it, everything else lands on it.

Network path requirement (NAT breaks the join). The sensor records the connection tuple as the kernel sees it on the agent host — for a pod, that's the pod IP and original ephemeral port. The collector must observe the same tuple, or the join silently never matches (workload stays empty, workload rules stay inert). In Kubernetes, run the collector in-cluster (pod-to-pod traffic keeps pod IPs on standard CNIs); if agents reach a collector outside the cluster, their egress is usually SNAT-masqueraded to the node IP and attribution will not resolve. Same caveat for any NAT device between agent hosts and the collector.

Where eBPF can't run (managed serverless like Lambda/Fargate), use in-agent identity below.

In-agent identity: the kilasec_agent package and identity tokens

Where you deploy this: Inside the agent process (or its container / Lambda layer). No dashboard action to install it — but token minting and revocation happen in the dashboard at Identity → Agent identity tokens. Package source: kilasec-agents/pyagent/ in the agents bundle.

Network-side attribution answers which host / container / binary sent a request. It cannot answer which of the fifty logical agents inside one orchestrator process sent it, and it cannot run at all in managed serverless. For those cases the identity comes from inside the agent process: the kilasec_agent Python package tags every outbound HTTP request the process makes with the agent's name — and, optionally, a Kilasec-issued identity token. The collector consumes and strips both headers before anything is forwarded upstream; they never leave your network.

Three ways to deploy it, in increasing order of zero-touch:

  1. One line in the agent: import kilasec_agent; kilasec_agent.init("billing-reconciler")
  2. One import, config from env: import kilasec_agent with KILASEC_AGENT=billing-reconciler set.
  3. No code changes at all: set PYTHONPATH=/opt/kilasec-agents/pyagent plus the same env vars in the systemd unit / Dockerfile / K8s manifest / Lambda layer — Python's sitecustomize hook loads the package before the agent's own code runs. This is how you tag agents whose source you don't own.

Identity tokens (verified identity). A name alone is self-declared — any process can claim it. Tokens close that gap:

  1. In the dashboard → Identity → Agent identity tokens → Mint token. One per agent. It's shown once — store it like a secret.
  2. On the agent host / in the agent's environment: set the value as KILASEC_AGENT_TOKEN.

The agent sends it on every request; the collector strips it and verifies it against the cloud. A valid token outranks every inferred signal — User-Agent, DHCP, even the self-declared name header — and the audit event records agent_source: token plus the token's fingerprint. Revoking a token on the same dashboard page takes effect at collectors within ~60 seconds.

An invalid or revoked token never blocks traffic: the flow just falls back to inferred attribution and the event is flagged agent_token_invalid — alert on that flag to catch stale or stolen tokens.

Tokens are attribution credentials, not access credentials — they grant nothing outside Kilasec. If some of the agent's destinations bypass the collector, scope injection with KILASEC_INJECT_HOSTS / KILASEC_INJECT_EXCLUDE so the token only rides on collector-inspected routes. Full env-var reference and framework recipes are in the package README (kilasec-agents/pyagent/README.md).

Documentation for kilasec — the AI Agent Firewall.