> ## Content Index
> Fetch the complete content index at: https://secdoc.tech/llms.txt
> Use this file to discover other available public pages before exploring further.

# Operator Attribution Across APIs, Proxies, and Automation
- URL: https://secdoc.tech/operator-attribution-across-apis-proxies-and-automation/
- Published: 2026-10-23T10:00:00.000Z
- Updated: 2026-10-23T09:59:59.000Z
- Description: Reliable operator attribution preserves the human initiator, delegated identities, automation executors, target subjects, trusted network context, and outcome across every hop.
- Author: secdoc
- Tags: audit-logging, identity, api-security, proxies, automation, opentelemetry, incident-response, cybersecurity, Basics Series, cybersecurity-basics

An audit log that says `admin changed policy` may be technically accurate and operationally useless.

The person clicked a button in a web interface. The browser called an API through a reverse proxy. The application placed work on a queue. A worker used a service credential to call a network controller. The controller recorded the service account as the actor. Several minutes later, a second worker verified the change.

Which identity belongs in the audit record?

All of the material identities do, but they must keep their roles. The human initiated the request. The web service accepted it. The job runner executed it. The service credential authenticated to the target. The target object was the subject of the change. A reviewer may have approved it. A break-glass administrator may have overridden a failed policy check.

Collapsing that chain into one `user` field loses the distinction between intent and execution. Copying an untrusted header into `actor` is worse because it creates attribution that an attacker can choose.

I treat operator attribution as a chain of custody for administrative intent. Every hop must preserve the parts it can verify, add its own execution facts, and refuse to promote claims that crossed an untrusted boundary.

## Define the identities before defining the log

The following roles recur in API and automation flows:

- `initiator` is the person or workload that requested the operation at the first trusted interaction.
- `approver` is the identity that authorized a gated operation. It may be absent.
- `delegator` is the principal that granted another identity authority to act.
- `executor` is the process, workload, or service account that performed the current hop.
- `authenticated_principal` is the identity proven by the current service's authentication mechanism.
- `subject` is the resource or identity being changed.
- `impersonated_principal` is an identity under which the system intentionally acted, when the platform supports impersonation.
- `break_glass_actor` is the operator who used an emergency path, with a separate reason and case reference.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Defining_Identities_in_the_Audit_Flow.png)

Define the identities before defining the log

These fields are not interchangeable. If a human launches a deployment and a runner uses a service account, the runner is the executor and authenticated principal at the target. The human remains the initiator if the chain can prove that link. Recording the human as though the target authenticated them would be false. Recording only the runner discards useful intent.

I use immutable identifiers alongside display values:

```yaml
principal:
  type: human
  issuer: https://identity.example.invalid/realms/operations
  subject_id: 4f9a7a58-4d2a-44e7-8ea8-4e67e790762a
  session_id: 0fb5cb3d-83a4-454d-91a3-86fcd5d54af1
  display_name: Example Operator
  assurance:
    authentication_context: phishing-resistant-mfa
    authenticated_at: 2026-09-13T14:02:11Z
```

The values are synthetic. The rule is not: `issuer` plus `subject_id` identifies the principal, while the display name exists for readers. Email addresses, usernames, token names, pod names, and IP addresses can change or be reused. They are attributes, not durable identity keys.

Workload identities need the same care. A generic `automation` user shared by every controller destroys attribution. I prefer a distinct workload identity for each execution boundary, with stable workload ID, environment, owner, credential ID, and deployment revision.

## Use one event envelope across hops

A normalized audit event should survive transport through APIs, queues, and workers without pretending that every field came from the same source.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/use_one_event_envelope_across_hops.png)

Use one event envelope across hops

```json
{
  "event_id": "aud_01J8RNYF19XB4P8ZC3K6M0V2QT",
  "event_type": "network.policy.update",
  "schema_version": 1,
  "occurred_at": "2026-09-13T14:03:24.612Z",
  "observed_at": "2026-09-13T14:03:24.701Z",
  "action": "update",
  "outcome": "succeeded",
  "initiator": {
    "type": "human",
    "issuer": "https://identity.example.invalid/realms/operations",
    "subject_id": "4f9a7a58-4d2a-44e7-8ea8-4e67e790762a",
    "session_id": "0fb5cb3d-83a4-454d-91a3-86fcd5d54af1"
  },
  "executor": {
    "type": "workload",
    "workload_id": "policy-controller-production",
    "service_principal_id": "spn_01J8RMWXP2W0FY5VQ9X3D7G6KS",
    "deployment_revision": "sha256:<image-digest>"
  },
  "authenticated_principal": {
    "issuer": "https://workload-identity.example.invalid",
    "subject_id": "spn_01J8RMWXP2W0FY5VQ9X3D7G6KS",
    "credential_id": "cred_01J8RN1NTEKBQYJ8CG2H7S4AXV"
  },
  "subject": {
    "type": "network-policy",
    "system": "network-controller",
    "resource_id": "policy_44891"
  },
  "request": {
    "operation_id": "op_01J8RNX9ME2H7AP5CK1Q6T4SZW",
    "correlation_id": "chg_01J8RNW6V2C9J5Q7TM4K0A3XPF",
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
    "source_ip": "192.0.2.44",
    "trusted_proxy_chain": ["198.51.100.20", "198.51.100.21"],
    "method": "PATCH",
    "route": "/v1/network-policies/{id}"
  },
  "authorization": {
    "decision": "allow",
    "policy_id": "admin-change-policy",
    "policy_revision": "2026.09.12.4",
    "decision_id": "dec_01J8RNY4NN8ZBFC0E7V16P2Q5S"
  },
  "change": {
    "before_digest": "sha256:<digest>",
    "requested_digest": "sha256:<digest>",
    "verified_after_digest": "sha256:<digest>"
  },
  "target_receipt": {
    "request_id": "controller-request-7830041",
    "status_code": 200,
    "verified": true
  }
}
```

The event separates the requested result from the verified result. A successful HTTP response is recorded, but `verified` becomes true only after the controller is read back and the relevant fields match. For rejected requests, I retain the requested action digest and the denial reason without storing secrets or full sensitive payloads.

`occurred_at` belongs to the system that performed the action. `observed_at` belongs to the collector. Both need synchronized clocks and explicit UTC offsets. Ordering should use durable operation and sequence data where available, not timestamps alone.

## Preserve initiator and executor through delegation

Delegation is where attribution usually collapses. A service receives a valid request from a human, then calls another service with its own credential. The downstream service can verify the service credential. It cannot independently verify the original human merely because the upstream service sent a header.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Neon_Delegation_Chain_Security_Dashboard.png)

Preserve initiator and executor through delegation

I represent the chain as linked assertions:

```yaml
attribution_chain:
  - hop: 1
    service: administration-api
    authenticated_principal: human:4f9a7a58-4d2a-44e7-8ea8-4e67e790762a
    mechanism: oidc-access-token
    assertion_strength: verified-locally
  - hop: 2
    service: job-orchestrator
    authenticated_principal: workload:administration-api
    initiator_assertion: human:4f9a7a58-4d2a-44e7-8ea8-4e67e790762a
    assertion_issuer: administration-api
    mechanism: signed-internal-token
    assertion_strength: verified-delegation
  - hop: 3
    service: network-controller
    authenticated_principal: workload:policy-controller-production
    initiator_assertion: human:4f9a7a58-4d2a-44e7-8ea8-4e67e790762a
    assertion_issuer: job-orchestrator
    mechanism: workload-token-plus-signed-job-envelope
    assertion_strength: verified-delegation
```

The downstream audit record states what it verified and who asserted the rest. It does not flatten the initiator into the authenticated principal.

A signed job envelope should bind the operation ID, initiator ID, originating issuer, explicit recipient or audience, intended executor class, approved action digest, managed-resource target, delegation-chain digest, token type and version, issue time, not-before time, expiry, and nonce. The worker verifies the signature and compares the approved digest with the work it is about to execute. A signature over `user_id` alone allows that identity claim to be detached from the intended action.

Short expiry limits replay. A nonce or operation ID stored with an idempotency record prevents duplicate execution. Signing keys have an issuer, key ID, validity period, and rotation history. Services reject unknown issuers, expired envelopes, altered digests, and delegation chains longer than policy allows.

If the chain breaks, the record should say `initiator_unverified` or `initiator_unknown`. Filling the gap with the service account owner, the last person who edited the workflow, or a guessed email address creates fiction.

## Proxy headers are claims at a trust boundary

Reverse proxies commonly add client address, scheme, host, and request identifiers. [The standardized](https://www.rfc-editor.org/rfc/rfc7239.html?ref=secdoc.tech) `Forwarded` header describes information added by proxies, including `for`, `by`, `host`, and `proto` parameters. Many deployments also use `X-Forwarded-For`, `X-Forwarded-Proto`, and vendor-specific headers.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Trusted_Headers_at_the_Network_Boundary.png)

Proxy headers are claims at a trust boundary

None of those fields is trustworthy merely because it has a familiar name.

The application must know which immediate peers are trusted proxies. The edge proxy must strip client-supplied forwarding and identity headers, then create canonical replacements. Each internal proxy should append only according to a documented policy. The application should reject direct traffic that bypasses the edge, or treat all forwarding headers on that path as untrusted.

```yaml
proxy_trust:
  accepted_immediate_peers:
    - edge-proxy-workload-identity
    - internal-gateway-workload-identity
  strip_at_edge:
    - Forwarded
    - X-Forwarded-For
    - X-Forwarded-User
    - X-Authenticated-User
    - X-Request-ID
  generate_at_edge:
    - Forwarded
    - X-Request-ID
  identity_propagation:
    method: exchanged-short-lived-internal-token
    header: X-Internal-Delegation
    audience: downstream-administration-api
    reject_unsigned_identity_headers: true
  maximum_hops: 3
```

I prefer authenticating proxies and services with mutual TLS or workload identity instead of trusting a source subnet alone. Network placement is useful defense in depth, but an IP address does not establish which proxy process made the claim.

Client IP is context, not operator identity. NAT, shared bastions, VPN concentrators, and forward proxies make that obvious. The address can support anomaly detection or incident scoping. It should not replace an authenticated subject ID.

The header parser also needs limits. Reject malformed quoting, duplicate parameters that violate policy, excessive hop counts, and oversized headers. Normalize IPv4, IPv6, and obfuscated identifiers according to the chosen contract. Log the raw header only in a restricted diagnostic store when required; ordinary audit events should contain the parsed trusted chain plus a digest of the source value.

## Trace context correlates work; it does not authorize it

[W3C Trace Context ](https://www.w3.org/TR/trace-context/?ref=secdoc.tech)defines `traceparent` and `tracestate` for propagating trace identity and vendor-specific trace data across distributed calls. It is excellent for finding the API request, queue publish, worker execution, and target call that belong to one operation.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Trace_Context_and_Audit_Boundaries.png)

Trace context correlates work; it does not authorize it

A trace ID is not an authenticated identity, an approval, or an idempotency key. An external caller may supply a trace header. Services can continue it for observability while marking the upstream trust level, or start a new internal trace and link the two. They should never authorize an administrative action because a trace ID matches a prior request.

I keep separate identifiers for separate jobs:

```plaintext
request_id       one received HTTP request
trace_id         distributed diagnostic trace
operation_id     one intended administrative operation
correlation_id   broader change, incident, or workflow
event_id         one immutable audit event
idempotency_key  duplicate side-effect control
approval_id      one authorization artifact
```

[OpenTelemetry's log data model](https://opentelemetry.io/docs/specs/otel/logs/data-model/?ref=secdoc.tech) includes timestamps, observed timestamps, severity, body, attributes, resource context, and trace correlation fields. That makes it useful for carrying normalized audit events from services to a collector. I put stable workload identity and deployment revision in resource attributes, action-specific data in event attributes, and high-cardinality identifiers only where the storage design expects them.

Audit evidence still needs its own schema and retention controls. A sampled trace can disappear. A lifecycle rule can drop verbose application logs. Neither behavior is acceptable for required administrative audit records.

## Record authorization as a decision artifact

Authentication answers which principal presented a valid credential. Authorization answers whether the action was allowed under a particular policy and context.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Authorization_As_a_Decision_Artifact.png)

Record authorization as a decision artifact

I record the policy decision without dumping the entire token or policy input:

```yaml
authorization:
  decision: allow
  decision_id: dec_01J8RNY4NN8ZBFC0E7V16P2Q5S
  policy_engine: opa
  policy_package: administrative.network_policy
  policy_revision: 2026.09.12.4
  principal_id: 4f9a7a58-4d2a-44e7-8ea8-4e67e790762a
  action: network-policy.update
  subject_id: policy_44891
  conditions:
    change_window: satisfied
    phishing_resistant_mfa: satisfied
    approval_required: true
  approval_id: apr_01J8RMZ0FA6G4YQ7B2H9P3V5NC
  reasons:
    - operator-role-permits-action
    - approved-change-matches-request-digest
```

OPA can produce policy decisions for this pattern, but the calling service must authenticate inputs and enforce the result. The decision record should identify the exact policy bundle or revision. A later policy update must not change the explanation of an earlier action.

Approvals bind to action and target digests. An approval for one firewall object cannot authorize a different object because both share a ticket number. When an operator edits the request after approval, the digest changes and approval returns to pending.

Break-glass events require more, not less, attribution: the emergency principal, normal principal if known, reason, case identifier, authentication method, time limit, actions performed, and post-use review status. The platform should never overwrite the emergency path with the operator's ordinary role in reports.

## Build target receipts and verification events

An administrative operation produces several audit facts:

```plaintext
request accepted
-> authorization decision
-> approval recorded, if required
-> job created
-> worker claimed job
-> target request sent
-> target response received
-> target state read back
-> operation closed or escalated
```

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Build_Target_Receipts_and_Verification_Events.png)

Build target receipts and verification events

I retain each as an immutable event linked by operation ID. A material change also records before, requested, and observed-after digests. For selected non-secret fields, a structured diff is more useful than digests alone:

```json
{
  "changes": [
    {
      "path": "/rules/7/action",
      "before": "allow",
      "requested": "deny",
      "observed_after": "deny"
    },
    {
      "path": "/revision",
      "before": 184,
      "requested": null,
      "observed_after": 185
    }
  ]
}
```

Secrets, bearer tokens, private keys, complete cookies, password fields, and sensitive request bodies do not belong in audit logs. I use an allowlist for recorded fields, then apply redaction before serialization. A redaction failure either blocks the high-risk administrative operation before execution or emits a minimal safe failure event while quarantining the unsafe payload in a separately controlled store. It never serializes the original secret-bearing object into the general audit pipeline.

If the target lacks a useful audit API, the automation receipt must be more rigorous, but it still cannot claim target-native attribution. I label evidence sources so reviewers can distinguish application logs, gateway logs, worker logs, and target audit records.

## Normalize without destroying provenance

A central pipeline makes searches and alerts practical, but normalization can erase distinctions. I keep the raw source name, source event ID, parser version, ingestion time, and original event digest alongside normalized fields.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Normalize_Without_Losing_Provenance.png)

Normalize without destroying provenance

```yaml
provenance:
  source_system: network-controller
  source_event_id: controller-event-993811
  source_event_time: 2026-09-13T14:03:24.612Z
  collected_at: 2026-09-13T14:03:26.104Z
  collector_id: audit-collector-production
  parser_version: audit-normalizer-1.8.2
  original_digest: sha256:<digest>
  normalization_warnings: []
```

[Keycloak](https://www.keycloak.org/docs/latest/server%5Fadmin/?ref=secdoc.tech#auditing-and-events) can emit user and administrator events, including administrative resource operations. [Authentik](https://docs.goauthentik.io/docs/sys-mgmt/events/?ref=secdoc.tech) also records system events and exposes event context for review. I map those into the shared model while preserving product event IDs and meanings. An IdP event may establish the login session that initiated an operation. It does not prove that a later target change succeeded.

Graylog or OpenSearch can store and search normalized events. Wazuh can alert on suspicious administrative activity and collect host evidence around the executor. OpenTelemetry can transport correlated records. I do not make every tool authoritative. The target audit source proves what the target recorded. The automation state store proves workflow progression. The IdP proves authentication events within its boundary. The policy engine proves the decision it returned.

Useful correlation queries include:

```plaintext
operation_id = <value>
| sort occurred_at, event_sequence
| fields event_type, source_system, initiator.subject_id,
         executor.workload_id, authenticated_principal.subject_id,
         subject.resource_id, outcome, verification.status
```

And for broken attribution:

```plaintext
event_type matches "*.admin.*"
and executor.type = "workload"
and initiator.subject_id is null
and attribution.status != "non-human-scheduled-operation"
```

A scheduled maintenance job may legitimately have no human initiator. Its initiator is a schedule definition with immutable ID, owner, approved revision, and next-run policy. Calling it "system" throws away those facts.

## Retention and integrity are part of attribution

[NIST SP 800-92](https://csrc.nist.gov/pubs/sp/800/92/final?ref=secdoc.tech) describes establishing log-management infrastructure and processes across an organization. For operator attribution, retention design must support investigations, access reviews, change disputes, and regulatory obligations without turning the audit platform into an uncontrolled archive of credentials and personal data.

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Retention_and_Integrity_Pipeline.png)

Retention and integrity are part of attribution

I define retention by event class. Privileged changes may need longer online and archive periods than routine read events. Authentication and proxy context may have different privacy constraints. Legal or incident holds suspend deletion for selected records through a documented process.

Normal administrators should not be able to alter their own audit history. I use append-only permissions, separate storage roles, immutable or retention-locked archives where required, encryption, key separation, and monitored export gaps. Hash chaining or signed batches can provide tamper evidence, but they do not replace access control or backups. Verification jobs must test that archived records can be retrieved and that signatures or digests still validate.

Retention starts only after delivery is proven. The application uses a durable local spool or transactional outbox so an audit collector outage does not erase the event. Backpressure has a defined response. For high-risk administrative changes, the system may refuse new work when durable audit capacity is exhausted. If it continues, the gap becomes an incident with exact start and end times.

## Test attribution by trying to break it

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Test_Attribution_Under_Fire.png)

Test attribution by trying to break it

The negative tests matter more than a screenshot of a populated dashboard:

1. Send `X-Forwarded-User` directly to the application. The edge strips it, and the application does not accept it as identity.
2. Connect around the trusted proxy. The service rejects the path or marks every forwarding claim untrusted.
3. Add a forged leftmost address to `X-Forwarded-For`. The parser derives the client only from the documented trusted chain.
4. Replay a signed job envelope. The idempotency and nonce store prevents a second side effect.
5. Change the target or action after approval. Digest comparison rejects execution.
6. Use an expired delegation token. The worker refuses the job and records the issuer and reason.
7. Remove the initiator field between queue publish and consume. Schema validation quarantines the message rather than substituting the executor.
8. Submit a valid trace ID from an unauthenticated caller. Correlation continues, but authorization receives no identity benefit.
9. Rotate a service credential. Historical records retain the old credential ID and workload identity without storing the secret.
10. Reuse a display name for another operator. Queries remain correct because correlation uses issuer and immutable subject ID.
11. Make the target return success without changing state. The operation closes as verification failed.
12. Stop the central collector. The outbox retains events, alarms on age, and drains without duplication after recovery.
13. Attempt to log a bearer token in a request field. The allowlist rejects or redacts it before export.
14. Alter one archived event. Integrity verification detects the changed batch or chain.
15. Use break glass. The emergency identity and reason remain visible through every downstream hop.

I also run ambiguity tests. Two identity issuers can produce the same subject string. Two proxies can use the same private address after a rebuild. A workload name can move between clusters. The schema must retain issuer, stable workload identity, and deployment context so those collisions do not merge unrelated actors.

## Handle failure without inventing certainty

Attribution quality needs an explicit status:

```yaml
attribution:
  status: partial
  initiator: verified-delegation
  executor: verified-locally
  target_principal: verified-by-target
  client_network_context: unavailable
  missing:
    - trusted-edge-request-id
  failure_reason: edge-log-export-delayed
  remediation_owner: platform-operations
```

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/Cybersecurity_Control_Room_Handling_Failure.png)

Handle failure without inventing certainty

The allowed states can include `complete`, `partial`, `conflicting`, `unverified`, and `not-applicable`. A conflict is preserved. For example, if the job envelope names one initiator while the operation store names another, the processor quarantines the action and records both assertions. It does not choose the more convenient value.

Collector delay, parser failure, unknown schema version, clock drift, signature failure, queue duplication, and target audit gaps each need alerts and repair paths. Reprocessing must preserve the original event ID and add parser or enrichment revisions. It must not make an old event look newly performed.

Dashboards should show attribution completeness by event type and execution path. I track privileged actions with unknown initiators, service accounts shared by multiple workloads, unmatched target receipts, unverified successful changes, proxy-chain parse failures, expired delegation attempts, audit-export lag, and archive verification age.

## The review questions I keep

Before accepting an administrative path, I ask:

1. Which principal did each hop authenticate directly?
2. Who initiated the operation, and which service vouches for that claim?
3. Which workload executed each side effect?
4. Which credential did the target accept?
5. What resource changed?
6. Which policy revision allowed it?
7. Was approval bound to the exact action and target?
8. Which proxy headers are accepted, and from which immediate peers?
9. Can a client bypass the proxy or inject an identity header?
10. Are request, trace, operation, event, and idempotency identifiers separate?
11. Does target read-back prove the requested state?
12. Can the evidence survive collector outage and administrator compromise?
13. How long is each event class retained, and who can retrieve it?
14. What happens when attribution is missing or contradictory?
15. Which negative test proves each trust claim?

![](https://storage.ghost.io/c/b9/e8/b9e8be96-d607-455c-b908-45c0663214eb/content/images/2026/10/The_Review_Questions_Attribution_Chain_Infographic.png)

The review questions I keep

Operator attribution is not a username column. It is a verified chain that separates human intent, approval, delegation, workload execution, target authentication, and observed outcome.

When that chain is intact, an incident responder can reconstruct a change without guessing from timestamps and IP addresses. When it is incomplete, the system says so while the evidence is still fresh enough to repair. That is far more useful than a polished audit dashboard built on one overloaded `actor` field.