Skip to content

Security Model

Security in Brokkr follows a defense-in-depth approach, implementing multiple layers of protection from network boundaries through application-level access controls. This document describes the trust boundaries, authentication mechanisms, authorization model, and operational security practices that protect Brokkr deployments.

Brokkr defines four distinct security zones, each with different trust levels and access controls. Understanding these boundaries is essential for secure deployment architecture and incident response.

C4Context
    title Brokkr Security Trust Boundaries (C4 System Context)

    Person(admin, "Admin Users", "Untrusted zone — must authenticate")
    System_Ext(generator, "Generators / CI", "Untrusted zone — must authenticate")
    System_Ext(internet, "Internet / External", "Untrusted zone")

    Enterprise_Boundary(dmz, "DMZ / Edge") {
        System_Ext(ingress, "Ingress Controller", "TLS termination and routing")
    }

    Enterprise_Boundary(trusted, "Trusted Zone") {
        System(broker, "Broker Service", "Authenticated API with authorization")
        SystemDb(db, "PostgreSQL", "Encrypted secrets, hashed credentials")
    }

    Enterprise_Boundary(semi_trusted, "Semi-Trusted Zone (per cluster)") {
        System(agent, "Brokkr Agent", "Scoped access — own resources only")
        System_Ext(k8s, "Kubernetes API", "Target cluster API server")
    }

    Rel(admin, ingress, "API requests", "HTTPS")
    Rel(generator, ingress, "Deployment requests", "HTTPS")
    Rel(internet, ingress, "External traffic", "HTTPS")
    Rel(ingress, broker, "Authenticated traffic", "HTTP :3000")
    Rel(broker, db, "Read/Write", "TCP :5432")
    Rel(agent, broker, "Poll & report", "HTTPS :3000")
    Rel(agent, k8s, "Manage resources", "HTTPS :6443")
  • Untrusted Zone — all external entities (internet traffic, admin clients, CI/CD generators); nothing is implicitly trusted and every request must authenticate.
  • DMZ — the edge where the ingress controller terminates TLS before traffic reaches application components.
  • Trusted Zone — the broker and its PostgreSQL database, communicating over internal networks; the database accepts connections only from the broker.
  • Semi-Trusted Zone — each target cluster’s agent, scoped to resources targeted to it and unable to see other agents’ resources, so a compromised agent can’t reach other clusters.

Four principles guide Brokkr’s security architecture:

Zero Trust by Default requires all API requests to authenticate; the middleware rejects uncredentialed requests before any handler runs. The anonymous surface is deliberately small but larger than health checks alone: the health checks (/healthz, /readyz) and Prometheus /metrics sit outside the auth middleware, and in builds that embed the operator console — which includes the published container image — so does the console itself. Every GET of a non-/api, non-/internal path on the broker port serves the console HTML, and that HTML carries a live read-only admin credential. Network reachability of the broker URL is therefore the console’s authentication boundary — see Read-Only Console Authentication for the design and the mitigations.

Least Privilege restricts each identity to the minimum permissions necessary — agents to their associated stacks (explicit targets plus label/annotation matches), generators to stacks they created.

Defense in Depth layers overlapping controls: network security, then application-level authentication, then authorization, then audit logging.

Immutable Audit Trail records every significant action and supports only create and read — no updates or deletions — so forensic evidence stays intact.

Brokkr recognizes four credential classes — admin, agent, generator, and the ephemeral read-only UI PAK that authenticates the operator console — each designed for different actor types and usage patterns.

PAKs serve as the primary authentication mechanism for agents and can also authenticate administrators and generators. The PAK design balances security with operational simplicity, enabling stateless authentication without storing plaintext secrets.

Every PAK follows a structured format that embeds both an identifier and a secret component:

brokkr_BR{short_token}_{long_token}
^ ^ ^
| | |
| | +-- Long token (secret, used for verification)
| +--------------- Short token (identifier, safe to log)
+------------------ Prefix (identifies key type)

A typical PAK looks like brokkr_BRabc123_xyzSecretTokenHere..., where abc123 is the short token (a loggable identifier) and xyzSecretTokenHere... is the long token (the secret used for verification).

PAK generation occurs when creating agents, generators, or admin credentials. The process uses cryptographically secure randomness from the operating system’s entropy source:

  1. The system generates a random short token of configurable length. This token uses URL-safe characters and serves as a loggable identifier; database lookups during verification key off the long-token hash, not the short token.

  2. A separate random long token is generated with sufficient entropy for cryptographic security. This token never leaves the generation response.

  3. The long token is hashed using SHA-256, producing a fixed-size digest that represents the secret without revealing it.

  4. The database stores only the hash. The original long token exists only in the complete PAK string returned to the caller.

  5. The complete PAK is returned exactly once. If the caller loses it, a new PAK must be generated—the original cannot be recovered. Because only the hash is stored, a database breach reveals nothing that can authenticate without the original long tokens.

When a request arrives with a PAK, the authentication middleware executes a verification sequence:

sequenceDiagram
    participant Client
    participant Middleware as Auth Middleware
    participant DB as PostgreSQL

    Client->>Middleware: Request with PAK header
    Middleware->>Middleware: Parse PAK, hash long token (SHA-256)
    Middleware->>Middleware: Compare against in-memory UI PAK (constant time)
    alt Not the UI PAK
        Middleware->>Middleware: Check auth cache (TTL, default 60s)
        alt Cache miss
            Middleware->>DB: Fetch admin role pak_hash, compare
            alt Not admin
                Middleware->>DB: Lookup agents by pak_hash (indexed)
                alt Not an agent
                    Middleware->>DB: Lookup generators by pak_hash (indexed)
                end
            end
        end
    end

    alt Matching record found
        Middleware-->>Client: Authenticated (continue to handler)
    else No match
        Middleware-->>Client: 401 Unauthorized (audit-logged)
    end

The middleware parses the presented PAK and hashes its long token with the same SHA-256 algorithm used at generation time. A malformed PAK is rejected with 401 at this step. It then resolves the identity by that hash, in order:

  1. UI PAK compare — the hash is compared against the process’s in-memory read-only UI PAK using a constant-time primitive; no database is involved. A match yields a read-only admin identity (see Read-Only Console Authentication).
  2. Auth cache — successful database-backed verifications are cached by hash for a configurable TTL (broker.auth_cache_ttl_seconds, default 60 seconds; 0 disables caching), so repeated requests from the same credential skip the database entirely.
  3. Database lookups — on a cache miss, the middleware fetches the admin role row and compares hashes, then performs an indexed lookup in the agents table by pak_hash, then in the generators table. The partial indexes on pak_hash exclude soft-deleted records, so lookups stay O(1) regardless of how many credentials exist.

Hash comparison uses a constant-time equality primitive (subtle::ConstantTimeEq) on the hex-encoded SHA-256 digests.

The auth cache has one operational consequence worth knowing: rotation and revocation through the REST API invalidate the cache entry immediately, but CLI-driven rotation (brokkr-broker rotate ...) operates directly on the database from a separate process and cannot reach a running broker’s cache — the old PAK may keep authenticating until the TTL expires. If that window is unacceptable, restart the broker after a CLI rotation. See Managing PAKs for details.

If verification succeeds, the middleware populates an AuthPayload structure identifying the authenticated entity (agent, generator, admin, or read-only admin) and attaches it to the request for downstream handlers. If verification fails, the request is rejected with a 401 status before reaching any route handler, and the failure is recorded as an auth.failed audit event.

Property Implementation
Secrecy Long token never stored; only SHA-256 hash persisted
Non-repudiation PAK uniquely identifies the acting entity
Revocation Entity can be disabled; PAK immediately invalid
Rotation New PAK generated via rotate endpoint; old one invalidated
Performance Indexed pak_hash lookup; O(1) regardless of credential count

Administrative users authenticate using PAKs stored in the admin_role table. Admin PAKs grant access to sensitive management operations that regular agents and generators cannot perform.

Admin authentication follows the same verification process as agent authentication, but the resulting AuthPayload sets the admin flag to true. Route handlers check this flag to authorize access to admin-only endpoints.

Terminal window
# Example admin API call
curl -X POST https://broker.example.com/api/v1/admin/config/reload \
-H "Authorization: Bearer brokkr_BR..."

Admin credentials should be treated with extreme care. A compromised admin PAK grants access to all system data, configuration changes, and audit logs. Organizations should implement additional controls around admin credential storage and usage, such as hardware security modules or secrets management systems.

Generators authenticate using PAKs stored in the generators table. Generator credentials enable CI/CD systems to create and manage deployments programmatically.

Generator permissions are scoped to resources they create. When a generator creates a stack, the broker records the generator’s ID with that stack. Future operations on the stack verify the requesting generator matches the owner. This scoping prevents one generator from modifying another’s deployments.

Terminal window
# Example generator API call
curl -X POST https://broker.example.com/api/v1/stacks \
-H "Authorization: Bearer brokkr_BR..." \
-H "Content-Type: application/json" \
-d '{"name": "my-stack"}'

Generators cannot access admin endpoints regardless of their PAK. The authorization layer checks identity type before granting access to protected routes.

Read-Only Console Authentication (the UI PAK)

Section titled “Read-Only Console Authentication (the UI PAK)”

The fourth credential class exists so the operator console works with zero configuration. At startup, the broker mints one random PAK per process and holds it in memory only — it is never persisted, never written to the database, and never logged. There is no rotation command for it; restarting the broker mints a fresh one.

In builds that embed the console (the published container image is built this way), the broker serves the console HTML for any GET of a non-/api, non-/internal path on its port, and injects the raw UI PAK into that HTML as a meta tag. The console picks it up and authenticates its API calls with it. The auth middleware recognizes the credential by an in-memory constant-time comparison and treats it as a read-only admin: it may GET or HEAD any endpoint — including admin-only listings across all tenants — plus exactly two POSTs that do not mutate desired state: credential introspection (POST /auth/pak) and diagnostic requests (POST /deployment-objects/{id}/diagnostics). Every other mutation is rejected with 403.

That injected token is the console’s only ambient credential, and the console has no persistent credential store. Through 0.8.x it also honoured an operator-pasted PAK in browser local storage (brokkr_pak) and preferred it over the injected token, so setting it silently promoted the entire console from read-only to full admin write — for every request, indefinitely, readable by any script on the origin that also serves the API. That override was removed in 0.9.0. Its stated purpose, giving an operator write access from the browser, is now served by a narrower mechanism: the console’s one privileged action, creating a generator tenant, prompts for an admin PAK at the moment it is used, sends it on that single request, and clears it — so no admin credential is ever at rest in a browser.

This design makes network reachability of the broker URL the console’s authentication boundary: anyone who can fetch the console page holds a working read-only admin credential. That is intentional — the console is an operations tool for people already inside the trust boundary — but it means the broker port must be treated as sensitive even for read access. As with /metrics, restrict it at the network level: limit who can reach port 3000 with a NetworkPolicy or firewall rules, and if the console must be exposed beyond the cluster, put authentication at the ingress (for example OIDC or basic auth on the ingress controller). Builds without the embedded console serve a plain placeholder page instead, with no credential.

Because each broker replica mints its own UI PAK, a token injected by one replica is rejected by another. Load-balanced deployments that expose the console therefore need sticky sessions (session affinity) so a browser keeps talking to the replica that issued its token.

For the operational side of this credential — reaching the console, what each of its views shows, the tenant scope selector, and the build that includes it — see Using the Operator Console.

Brokkr implements implicit role-based access control (RBAC) where roles are determined by authentication type rather than explicit role assignments.

Role Authentication Capabilities
Agent PAK via agents table Read targeted deployments, report events, claim work orders
Generator PAK via generators table Manage own stacks and deployment objects
Admin PAK via admin_role table Full system access including configuration and audit logs; cannot bypass generator registration when targeting stacks
Read-only admin (UI PAK) Ephemeral in-memory PAK, minted per broker process GET/HEAD on any endpoint (admin-level visibility), plus POST /auth/pak and POST /deployment-objects/{id}/diagnostics; all other mutations rejected with 403
System Internal only Background tasks, automated cleanup

Registration enforcement — the requirement that an agent be registered with a generator before explicit targets can be created within that generator’s stacks — applies uniformly to all identities, including admins. There is no bypass or force flag. See Generator Registration and Application Scopes below.

The following table summarizes which roles can access each API endpoint category:

Endpoint Pattern Agent Generator Admin
/api/v1/agents/{id}/target-state Own ID only No Yes
/api/v1/agents/{id}/events Own ID only No Yes
/api/v1/agents/{id}/work-orders/* Own ID only No Yes
/api/v1/agents/{id}/registrations Own ID only No Yes
/api/v1/generators/{id}/register (POST/DELETE) Own ID only No Yes
/api/v1/generators/{id}/registered-agents No Own ID only (not the system generator) Yes
/api/v1/stacks/* No Own stacks Yes
/api/v1/agents (GET, listing) No Own registered agents (not the system generator) Yes (whole fleet)
/api/v1/agents/* (management) No No Yes
/api/v1/admin/* No No Yes
/api/v1/webhooks/* No No Yes
/api/v1/paks (GET) No No Yes (the read-only UI PAK qualifies)
/healthz, /readyz Public (no auth) Public (no auth) Public (no auth)
/metrics Public (no auth) Public (no auth) Public (no auth)
Console routes (any non-/api GET, embed-ui builds) Public (no auth) Public (no auth) Public (no auth)

Two of those rows are tenant-scoped reads rather than all-or-nothing gates. GET /api/v1/agents returns the whole fleet to an admin and, to a generator, exactly the agents registered with that generator — so a tenant can answer “which agents serve me?” without an admin credential. GET /api/v1/generators/{id}/registered-agents answers the narrower question with registration records (agent ids and timestamps) rather than agent detail; use the agent listing when you need names, clusters, status, or heartbeats.

Neither is available to the system generator, which is why both rows carry that exclusion. __system__ is internal delivery infrastructure rather than a tenant, and every agent is auto-registered with it — so scoping either read to it would return the entire fleet through a non-admin credential. Both endpoints reject it with system_generator_not_a_tenant (403). This is defense in depth: the system generator is provisioned without a PAK, so nothing can authenticate as it today.

The read-only UI PAK is not a separate column: it passes every admin read check in the table (GET/HEAD), plus the two allowlisted POSTs, and fails everything else with 403. GET /api/v1/paks lists the non-system generators as {id, name} pairs so the console can offer a tenant selector; the ?pak_id= parameter it feeds on /fleet, /stacks, and /agent-events is a view filter, not an authorization boundary — real isolation comes from generator ownership checks, not from pak_id.

Note that /metrics is mounted outside the authentication middleware, exactly like /healthz and /readyz—any client that can reach the broker port can scrape it. Metrics can reveal operational details (request rates, agent counts), so restrict access at the network level: use a NetworkPolicy (networkPolicy.allowMetricsFrom) or firewall rules to limit scraping to your monitoring infrastructure.

The console routes are likewise mounted outside the auth middleware in embed-ui builds, and the page they serve embeds a live read-only admin credential (details above). Anyone who can reach the broker port gets read-only admin visibility, so apply the same network-level restrictions — NetworkPolicy or firewall rules on port 3000, or ingress-level authentication when exposing the console beyond the cluster — and use sticky sessions when running multiple replicas.

Beyond endpoint-level authorization, Brokkr enforces resource-level access control through database queries.

Agent Scope limits agents to resources from stacks associated with them. When an agent requests deployment objects, the broker resolves the agent’s associated stacks as the union of explicit targets (agent_targets rows), stacks sharing any of the agent’s labels, and stacks sharing any of the agent’s annotations—then serves only those stacks’ objects. The label and annotation legs of that union are themselves confined to generators the agent is registered with, so matching narrows within an agent’s consented scopes rather than reaching outside them.

This resolution happens server-side on every request, so agents can never see deployment objects from stacks outside that union, regardless of what parameters they provide in API requests.

Registration is the consent boundary on both sides of that union. An explicit target can only be created when the agent is registered with the stack’s owning generator — an attempt to target a stack whose generator the agent is not registered with fails with agent_not_registered (HTTP 403). Label and annotation matching is filtered by the same rule at read time, so a stack reaches an agent through a selector only when the agent has registered with that stack’s generator. An agent with no registrations receives nothing from matching at all, and two tenants can reuse the same label vocabulary without colliding. See Generator Registration and Application Scopes.

An agent has a status. The CLI and the console set it to ACTIVE or INACTIVE. A new agent starts INACTIVE. The status is a convention that the agent keeps. It is not a boundary that the broker enforces.

  • The agent enforces it. The agent reads its own record on each heartbeat. While the status is not ACTIVE, the agent does not fetch deployment objects, work orders, health checks, diagnostics or webhook deliveries. A change of status has an effect in one poll cycle.
  • The broker does not enforce it. GET /agents/{id}/target-state has no status filter. The broker serves the same deployment objects to an INACTIVE agent and to an ACTIVE agent. The broker uses the status only for the brokkr_active_agents metric and in event and audit data.

Thus a changed agent binary, or an older one that does not check its status, gets work while it is INACTIVE. Do not use INACTIVE to contain an agent that you do not trust. To stop such an agent, revoke or rotate its PAK (see Credential Revocation), or block it at the network.

Generator Scope restricts generators to stacks they created:

SELECT * FROM stacks
WHERE generator_id = :requesting_generator_id
AND deleted_at IS NULL;

Generators cannot list, read, or modify stacks created by other generators or through admin operations.

Generator Registration and Application Scopes

Section titled “Generator Registration and Application Scopes”

Generator ownership scopes what a generator can touch; registration scopes which agents a generator’s stacks can reach. A generator is an application scope, and an agent must be registered with that generator before any stack the generator owns can be targeted at the agent. Registration is the agent’s opt-in consent boundary — it prevents one application from accidentally (or maliciously) scheduling its workloads onto agents that never agreed to run them, an application-level tenancy boundary inside a single broker.

Registration gates both the creation of explicit targets and read-time resolution. Adding a target (POST /agents/{id}/targets) or removing one (DELETE /agents/{id}/targets/{stack_id}) is rejected with agent_not_registered (HTTP 403) when the agent is not registered with the stack’s owning generator. This check is enforced uniformly across every identity — there is no admin override or force flag. On the read path (GET /agents/{id}/target-state), the label and annotation legs of the served-stack union are restricted to stacks owned by generators the agent is registered with; explicit targets need no read-time check, having been gated at creation. The practical consequence is that a generator declares which labels it publishes to, an agent’s registrations declare which generators it accepts work from, and no selector can cross that line.

Two scopes deserve special mention:

  • System generator (__system__, is_system=true) — provisioned at broker startup and excluded from the public GET /generators listing. Every agent is automatically registered with it at creation, so the fleet/system stacks it carries reach all agents without per-agent opt-in. It is not the admin generator.
  • Admin generator — a separate entity tied to the admin role and its PAK. Agents are not auto-registered with it; an admin-owned stack still requires explicit registration before it can be targeted, which is why admins cannot bypass the registration gate.

Agents can be pre-registered when they are created (POST /agents accepts an optional list of generator IDs) in addition to the automatic system-generator registration, or registered later by the agent acting on itself or by an admin. Deregistration is destructive and cascades: removing a registration deletes the agent’s explicit targets for that generator’s stacks and pushes a TargetChanged frame over the agent’s WebSocket, so the agent prunes the corresponding Kubernetes resources on its next reconcile.

Operational steps for registering and deregistering agents live in the Agent Registration how-to guide. This application-level isolation is complementary to, and distinct from, the deployment-level schema-per-tenant isolation that separates whole brokers — registration partitions agents within a single broker, while a per-tenant schema partitions the data store itself.

Brokkr stores credentials using appropriate protection levels based on sensitivity:

Credential Type Storage Location Protection
PAK hashes PostgreSQL SHA-256 hash (plaintext never stored)
Webhook URLs PostgreSQL AES-256-GCM encryption
Webhook auth headers PostgreSQL AES-256-GCM encryption
Database password Kubernetes Secret Base64 encoding (use sealed-secrets in production)
Webhook encryption key Environment variable Should use Kubernetes Secret

Webhook URLs and authentication headers may contain sensitive information like API keys or tokens. Brokkr encrypts these values at rest using AES-256-GCM, a modern authenticated encryption algorithm.

The encryption format includes version information for future algorithm upgrades:

version (1 byte) || nonce (12 bytes) || ciphertext || tag (16 bytes)

The current version byte (0x01) indicates AES-256-GCM encryption. The 12-byte nonce ensures each encryption produces unique ciphertext even for identical plaintexts. The 16-byte authentication tag detects any tampering with the encrypted data.

The encryption key is configured via the BROKKR__BROKER__WEBHOOK_ENCRYPTION_KEY environment variable as a 64-character hexadecimal string (representing 32 bytes). If no key is configured, the broker generates a random key at startup and logs a warning. Production deployments should always configure an explicit key to ensure encrypted data survives broker restarts.

Legacy format and its inherent ambiguity (read-only)

Section titled “Legacy format and its inherent ambiguity (read-only)”

Data written before the versioned format is a headerless nonce (16 bytes) || ciphertext XOR blob, supported read-only for one-time migration (a leading version byte 0x00 also routes to this path). Because a headerless blob has no version header, the decryptor falls back to the legacy path whenever the first byte is not a known version marker.

This leaves a small, inherent ambiguity: a legacy blob whose nonce happens to begin with 0x00 (legacy marker) or 0x01 (AES-GCM marker) is mis-routed and fails to decrypt. The window is tiny — roughly 2/256 (~0.78%) of legacy blobs — and is bounded entirely to the read-only migration path. It cannot affect new data: AES-256-GCM ciphertext always carries the 0x01 version byte and is dispatched unambiguously. The ambiguity is not fixable without a format change (headerless data is inherently undistinguishable from versioned data in that one byte), so the resolution is operational: let the migration re-encrypt legacy values into the versioned (0x01) format, after which the ambiguity no longer applies.

PAK rotation replaces an entity’s authentication credential without disrupting its identity or permissions. The POST /api/v1/agents/{id}/rotate-pak endpoint (and similar endpoints for generators) generates a new PAK and invalidates the previous one atomically.

After rotation, the old PAK becomes invalid immediately — the endpoint updates the stored hash, invalidates the broker’s auth cache, and closes any open WebSocket authenticated with the old credential, all before returning the new PAK (which is shown once and cannot be retrieved again). The entity must then be reconfigured with the new PAK before its next broker communication. Step-by-step rotation procedures are in Managing PAKs.

Revoking access involves soft-deleting the associated entity. When an agent is deleted, its record remains in the database with a deleted_at timestamp, but authentication queries filter out deleted records:

Terminal window
# Revoke agent access
curl -X DELETE https://broker/api/v1/agents/{id} \
-H "Authorization: Bearer <admin-pak>"

After revocation, the agent’s PAK becomes invalid immediately. Existing deployments remain in the target cluster (Brokkr doesn’t forcibly remove resources), but the agent can no longer fetch new deployments or report events.

The audit logging system records significant actions for security monitoring, compliance, and forensic analysis. The system prioritizes completeness and immutability while minimizing performance impact on normal operations.

The audit logger uses an asynchronous design to avoid blocking API request handlers:

API Handler → mpsc channel (10,000 buffer) → Background Writer → PostgreSQL

When an action occurs, the handler sends an audit entry to a bounded channel. A background task collects entries from this channel and writes them to PostgreSQL in batches. This design ensures audit logging never blocks request processing, even under high load.

The background writer batches entries for efficiency, flushing when the batch reaches 100 entries or after 1 second, whichever comes first. This batching reduces database round trips while ensuring entries are persisted within a predictable time window.

Each audit log entry captures comprehensive context about the action:

Field Description
timestamp When the action occurred (UTC)
actor_type Identity type: admin, agent, generator, or system
actor_id UUID of the acting entity (if applicable)
action What happened (e.g., agent.created, pak.rotated)
resource_type Type of affected resource
resource_id UUID of affected resource (if applicable)
details Structured JSON with action-specific data
ip_address Client IP address (when available)
user_agent Client user agent string (when available)

The audit system records lifecycle events for agents, generators, templates, stacks, work orders, and webhook subscriptions; PAK issuance and rotation (pak.created/pak.rotated, via REST or CLI); webhook deliveries that exhaust retries (webhook.delivery_failed); failed authentication attempts (auth.failed, with source IP and request path); and configuration reloads (config.reloaded, with the change set). Two constants are intentionally unrecorded — auth.success (volume) and pak.deleted (PAKs die with their entity) — see the Audit Logs Reference.

The audit log API supports filtering to find relevant entries:

Terminal window
# Recent authentication failures
curl "https://broker/api/v1/admin/audit-logs?action=auth.failed&limit=100" \
-H "Authorization: Bearer <admin-pak>"
# Actions by specific agent
curl "https://broker/api/v1/admin/audit-logs?actor_type=agent&actor_id=<uuid>" \
-H "Authorization: Bearer <admin-pak>"
# All admin actions in a time range
curl "https://broker/api/v1/admin/audit-logs?actor_type=admin&from=2024-01-01T00:00:00Z" \
-H "Authorization: Bearer <admin-pak>"

Filters support actor type and ID, action (with wildcard prefix matching), resource type and ID, and time ranges. Pagination limits responses to manageable sizes, with a maximum of 1000 entries per request.

Audit logs are retained for a configurable period (default: 90 days). A background task runs daily to remove entries older than the retention period. This cleanup prevents unbounded database growth while maintaining sufficient history for security investigations.

The retention period is configurable via broker settings, allowing organizations to meet their specific compliance requirements.

Both broker and agent deployments configure security contexts that follow Kubernetes security best practices:

podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false # Configurable
capabilities:
drop:
- ALL

Non-root execution prevents containers from running as the root user, limiting the impact of container escape vulnerabilities. The UID 10001 is arbitrary but consistent across components.

Privilege escalation prevention blocks processes from gaining additional capabilities after container startup, closing a common attack vector.

Capability dropping removes all Linux capabilities by default. Most applications don’t need capabilities, and removing them reduces attack surface.

AppArmor support is available when enabled via apparmor.enabled: true. AppArmor provides mandatory access control at the kernel level, further restricting what container processes can do.

The agent requires Kubernetes permissions to manage resources in target clusters. The Helm chart creates RBAC resources that implement least-privilege access:

Resource management access to core resources (pods, services, configmaps, deployments, and others) enables the agent to apply, update, and delete resources as part of deployment management. The agent requires create, get, list, watch, update, patch, and delete permissions on the resources it manages.

Shipwright access (when enabled) grants create, update, and delete permissions on Build and BuildRun resources for container image builds.

Optional secret access is disabled by default. When enabled via rbac.secretAccess.enabled, the agent can list and watch secrets. The rbac.secretAccess.readContents flag controls whether the agent can read actual secret values.

The RBAC configuration supports both namespace-scoped (Role/RoleBinding) and cluster-wide (ClusterRole/ClusterRoleBinding) permissions, configured via rbac.clusterWide. Namespace-scoped mode (rbac.clusterWide: false) deploys within its namespace, and the chart automatically sets BROKKR__AGENT__WATCH_NAMESPACE so telemetry streaming and health discovery operate in-namespace too. Remaining constraints: reconciliation pruning skips resource types it cannot list, and stacks containing cluster-scoped resources (Namespaces, CRDs) fail to apply.

Operational hardening guidance—the production deployment checklist, security monitoring thresholds, and incident response procedures for suspected agent or broker compromise—lives in the Security Hardening how-to guide.

Brokkr implements several controls relevant to data protection regulations, summarized in the mapping below.

Requirement Brokkr Feature
Access control PAK authentication + implicit RBAC
Audit trail Immutable audit logs with comprehensive action recording
Data encryption TLS in transit (terminated at the ingress/mesh — the broker itself serves plain HTTP), AES-256-GCM for secrets at rest
Least privilege Scoped agent and generator access
Monitoring Metrics endpoint, audit log queries
Incident response Credential revocation, audit log forensics