This is the full developer documentation for Riptides # Controlling Agent Access Access Control is where you set guardrails on your AI agents: which **models** each agent may call on a given provider, which **tools** it may use on a given connector, and whether those rules apply when the agent runs autonomously or when a specific person is driving it. By default everything is allowed; you add **restrictions** only for the providers and connectors you want to narrow, and Riptides enforces those inline before the request reaches the provider. Open it from **Agentic → Access Control** in the sidebar. ## How access is decided [Section titled “How access is decided”](#how-access-is-decided) Three ideas drive everything on this screen. **Default allow, restrict where you add a rule.** An agent with no policy, or an empty policy (no rules), has **full access**: it can use any model and any tool. Traffic to a provider or connector stays allowed unless the policy contains an explicit rule for that service. When you add a rule for a provider or connector, that service becomes restricted: only the models or tools you list there are permitted, and everything else on that service is blocked. Providers and connectors you never add a rule for remain unrestricted. So this is not a global denylist. It is always-allow, except where you place a restriction. **A policy targets one agent, optionally scoped to one person.** For a single agent (workload identity) you can have: * an **Autonomous Operation** policy: the rules that apply when the agent runs with no human driving it; and * one policy **per user**: rules that apply only when that specific person is driving the agent. **Which policy applies depends on who is driving the agent, and there is no fallback between them:** | Who is driving | Policy used | | ---------------------------------------------- | ---------------------------------------------------------------------- | | No one (autonomous) | Autonomous Operation policy, if it exists | | A user who has their own policy for this agent | That user’s policy | | A user with **no** user policy for this agent | Full access (pass-through), even if Autonomous Operation is restricted | So a restricted Autonomous Operation policy does **not** constrain human-driven sessions unless you also create a user policy for that person. This lets you, for example, lock the agent down when it runs unattended while giving a senior engineer a broader (or separate) set of restrictions when they drive it, but anyone without a user policy remains unrestricted. ## The workloads list [Section titled “The workloads list”](#the-workloads-list) The dashboard lists each agent (workload identity) that has policies, with: * **Workload Identity**: the agent, such as `claude` or `codex`. * **LLMs** and **Connectors**: how many providers and connectors its policies restrict. * **Autonomous**: whether the agent’s autonomous operation is **Restricted** or **Not restricted** (full access when running on its own). ![The Access Control dashboard listing governed workloads](/_astro/list.DOUbQTKG_1UMJp9.webp) Agents are [workload identities](../../concepts/workload-identity); an agent must exist as a workload identity before you can write policy for it. ## Create a policy for an agent [Section titled “Create a policy for an agent”](#create-a-policy-for-an-agent) 1. Choose **Add policy**. 2. Select the **workload identity** (the agent) you want to govern. 3. Confirm. This creates an empty policy (full access), which you then narrow in the editor. Clicking a workload row opens the **policy editor** for that agent. ## Choose what the rules apply to [Section titled “Choose what the rules apply to”](#choose-what-the-rules-apply-to) The editor first asks *who* the rules apply to: * **Autonomous Operation**: a fixed entry at the top. Select it to restrict the agent itself. * **A specific user**: select **Restrict user** and pick a person from your [Users](../users). Their policy then governs the agent only when they are driving it. The footer shows how many of your users already have a restricted policy. To remove a user’s restriction, delete their policy from this list. ## Restrict models and tools [Section titled “Restrict models and tools”](#restrict-models-and-tools) Once you have selected Autonomous Operation or a user, the editor shows two tabs, each with a count of how many entries are restricted: ![The policy editor restricting a workload\'s models and tools](/_astro/detail.7QPh3h_k_Z25myqw.webp) ### LLM Models [Section titled “LLM Models”](#llm-models) Lists the LLM providers the policy restricts. Expand a provider to see its models, each with a toggle: * Turn individual models **off** to keep only the ones you leave on for that provider. * Leaving every model of a provider on allows the models known for that provider at save time (the list is stored explicitly). * Removing a provider from the policy lifts the restriction, so that provider is allowed again in full. To deny models on a provider, keep the restriction and turn those models off (or leave none on). Use **Add restriction** to add a provider to the policy (choosing from the providers on your [LLMs](../llms) screen). ### Connectors [Section titled “Connectors”](#connectors) Lists the MCP servers the policy restricts. Expand a connector to see its tools, each with a toggle, and use **Add restriction** to add a connector (from your [Connectors](../connectors) screen). The behavior mirrors LLM Models: once a connector has a restriction, only the tools you leave on are allowed for that connector (disallowed tools are blocked and hidden from the agent). Connectors you never restrict remain fully allowed. When you are done, choose **Save**. The footer summarizes the counts and confirms the save. ## Restrict autonomous operation [Section titled “Restrict autonomous operation”](#restrict-autonomous-operation) To limit what an agent can do when it runs on its own, edit its **Autonomous Operation** policy and **Add restriction** only for the providers and connectors you want to narrow when no one is driving it, then leave on only the models and tools you allow unattended. Once those restrictions are in place, the **Autonomous** column on the dashboard shows the agent as **Restricted**. Leaving the Autonomous Operation policy empty, or omitting a provider or connector from it, leaves that traffic fully allowed for autonomous runs. Remember: this policy applies only when there is no human driver; it does not cover users who lack their own policy. ## Restrict to specific users [Section titled “Restrict to specific users”](#restrict-to-specific-users) To govern a person when they drive the agent, add a **user** policy (via **Restrict user**) and set its restrictions. That policy takes effect only for that user. It does **not** inherit or fall back to Autonomous Operation: other users without their own policy keep full access, and autonomous runs use only the Autonomous Operation policy. A user is matched by the identity assigned on the [Users](../users) screen. ## What a block looks like [Section titled “What a block looks like”](#what-a-block-looks-like) When an agent tries to use a model or tool that a restriction for that service does not permit, Riptides blocks it before the request leaves the host, and the block is recorded. In the [Activity Monitor](../activity-monitor) the blocked model or tool is flagged on its session, and the **Blocked events** count goes up. Open Access Control for that workload (and the Autonomous Operation or user entry that matches who was driving) to adjust the restriction if the block was not intended. ## Manage policies as code [Section titled “Manage policies as code”](#manage-policies-as-code) Policies are standard Riptides resources (`TrafficPolicy`), so you can also manage them with the CLI instead of the console. This is useful for GitOps or bulk changes. A policy names the agent (`workloadID`), an optional user (`actorID`), and its LLM and connector restrictions: ```yaml apiVersion: core.riptides.io/v1alpha1 kind: TrafficPolicy metadata: name: claude-autonomous namespace: riptides-system spec: workloadID: claude # actorID omitted: this is the Autonomous Operation policy. # A user-scoped policy sets actorID to that user's identity. llmRules: - llmRef: name: anthropic-api models: - claude-haiku-4-5-20251001 connectorRules: - serverRef: name: mcp-linear tools: - search_issues - list_teams ``` Apply it with: ```bash riptides-cli ctl apply -f claude-autonomous.yaml ``` An empty `spec` (no rules) means full access. Each listed rule restricts only that provider or connector to the models or tools named in it; any service without a rule stays fully allowed. There is one policy per agent-and-user pair (omit `actorID` for Autonomous Operation). ## Next steps [Section titled “Next steps”](#next-steps) * [Monitor AI activity](../activity-monitor): confirm your policy by watching sessions and blocked events. * [LLMs](../llms) and [Connectors](../connectors): keep the models and tools your policies reference up to date. Current limitations Access Control is written and tested for **Claude Code** and **Codex**. It can also work with other LLM-based applications, but policy recognition and enforcement may be incomplete for them, for example models or tools that are not identified the same way, or calls that are not filtered as they are for those agents. The same coverage limits apply to the [Activity Monitor](../activity-monitor). # Monitoring AI Activity The Activity Monitor shows what your AI agents are doing. Every agent run is captured as a **session**: the models it used, the tools it called, the services behind those tools, the person who drove it, and anything a policy blocked. You can browse sessions at a glance, then open one to see it as a graph or as a full conversation. Open it from **Agentic → Activity Monitor** in the sidebar. The point is **attribution**. Agents act semi-autonomously and reach real systems through tools, so when something happens you need to trace it back: which agent, driven by which person, reached which resource through which tool, and in what context. The Activity Monitor gives you that lineage. It is not about watching employees use an LLM, it is about holding autonomous agents accountable for what they do. Note AI activity is recorded and retained within your Riptides environment, which is where the Activity Monitor reads it from. ## The sessions dashboard [Section titled “The sessions dashboard”](#the-sessions-dashboard) The dashboard lists every AI session in the selected time window. Use the time-range control (top right) to switch between the last **1 hour**, **6 hours**, **24 hours** (the default), or **7 days**, and the refresh button to reload. Queries are capped at the last seven days. ![The Activity Monitor sessions dashboard](/_astro/list.k5qHmEja_Z1thRze.webp) At the top, four cards summarize the window: * **Sessions**: how many agent runs occurred. * **Input tokens** and **Output tokens**: total token usage across all sessions. * **Blocked events**: how many models or tools were blocked by policy. Below the cards, two usage panels rank the most-used **Models** and **Services / APIs** in the window. The table lists the sessions themselves. Each row shows: * **Session**: the agent’s workload identity (for example `claude` or `codex`), the person who drove it, and the session id. Sessions that contained blocked activity are flagged. * **Started** and **Duration**: when the run began and how long it lasted. * **Exchanges**: how many request/response turns the agent had with an LLM. * **Tokens in / out**: token usage for the session. * **Models**: the models the agent used. * **Services**: the MCP services it reached. Search by the person driving the session, and click any row to open it. ## Opening a session [Section titled “Opening a session”](#opening-a-session) A session opens in a side panel with a summary strip (exchanges, input and output tokens, duration, and start time), plus a thinking indicator when the agent used reasoning and a blocked indicator when policy stepped in. ![A session\'s detail view](/_astro/session-detail.DGZkxJQA_2cHpN8.webp) You can view the session two ways, using the toggle at the top of the panel: * **Graph** (the default): a visual map of what the agent talked to. * **Conversation**: the full turn-by-turn transcript. ## The session graph [Section titled “The session graph”](#the-session-graph) The graph reads left to right, in five columns: * **Who**: the person who drove the agent. * **Workload**: the agent identity that made the calls (for example `claude`). * **Models**: each model the agent used. A spawned sub-agent gets its own node, badged with the sub-agent it belongs to. * **Tool calls**: each distinct tool the agent invoked. MCP tools are badged **MCP**; built-in tools show as local tools. * **Services**: the registered service behind an MCP tool call (the same entity you see in Service Inventory). Only MCP tool calls have a service. ### Focusing on a path [Section titled “Focusing on a path”](#focusing-on-a-path) Click any node to focus it. The graph highlights that node’s full lineage, everything upstream and downstream of it, and dims the rest, so you can isolate, say, one model and just the tools it called. Use **Reset zoom** to fit the whole graph again. ### Node details [Section titled “Node details”](#node-details) Clicking a **model** or a **tool** node opens a detail drawer on the right: * **Tool drawer**: the tool name, whether it is an MCP or local tool (and which server), the total number of calls, and how many were blocked. Below that is every call, newest first: when it happened, which model made it, and, for each call, its **input** (arguments) and **output** (result). Blocked calls are marked as blocked. * **Model drawer**: the model name, the sub-agent it belongs to (if any), the number of exchanges, and how many were blocked, followed by one row per exchange. ## Trace a block to its policy [Section titled “Trace a block to its policy”](#trace-a-block-to-its-policy) When a model or tool is blocked, the detail drawer marks that call as blocked. Note the session’s workload identity and who was driving it, then open [Access Control](../access-control) for that agent and select the matching Autonomous Operation or user policy. From there you can adjust the allowlist if the block was not what you intended. ## The conversation view [Section titled “The conversation view”](#the-conversation-view) Switch to **Conversation** to read the session as a transcript. Prompts appear as chat bubbles, the person’s messages on one side and the model’s responses on the other, with reasoning (“thinking”) shown separately and tool calls as expandable rows that reveal their arguments and results. Blocked tool calls are marked. By default the transcript focuses on the interesting parts and collapses routine turns; toggles let you show **all exchanges**, include **system** prompts, and fold background sub-agent activity into groups. Use the search box to find a specific message. ## Next steps [Section titled “Next steps”](#next-steps) * [Control agent access](../access-control): turn what you saw into a policy that allows only the models and tools you intend. * [Manage LLMs](../llms) and [Connectors](../connectors): review the providers and MCP servers agents can reach. Current limitations The Activity Monitor is written and tested for **Claude Code** and **Codex**. It can also work with other LLM-based applications, but you may see gaps: incomplete sessions, missing exchanges, or tool and model details that do not reconstruct cleanly. The same coverage limits apply to [Access Control](../access-control). # Connectors The **Connectors** screen lists the MCP servers registered in your system: the tool integrations your agents can use. MCP (Model Context Protocol) is how an agent calls out to real systems: creating an issue in Linear, opening a pull request, sending a Slack message. Each connector exposes a set of **tools**, and those tools are what you allow or restrict in an [access policy](../access-control). Open it from **Agentic → Connectors** in the sidebar. ## Built-in connectors [Section titled “Built-in connectors”](#built-in-connectors) Riptides ships with definitions for many common MCP servers, so their tools are ready to reference in policy without any setup. The built-in set includes: * **Linear** (`mcp.linear.app`): `create_issue`, `update_issue`, `search_issues`, `list_teams`, and more * **GitHub Copilot**, **Slack**, **Notion**, **Atlassian**, **Stripe**, **HubSpot**, **Sentry**, **PagerDuty**, **Figma**, **Supabase**, **Cloudflare**, **Zapier**, and others Each connector is matched by its MCP endpoint and carries the list of tools it exposes. ## What you see [Section titled “What you see”](#what-you-see) ![The Connectors dashboard](/_astro/list.7xo68uvL_ZyraNI.webp) Three cards summarize the inventory: **Total servers**, **Connected** (active), and **Total tools**. The table lists each connector with: * **Status**: whether the server is active. * **Server**: its display name and description. * **Endpoint**: the MCP host it matches. * **Tools**: how many tools it exposes. * **Categories**: tags used to group connectors. Search by name, and filter by status or category. ## Add a connector [Section titled “Add a connector”](#add-a-connector) Use **Add connector** to register an MCP server that is not built in. You give it a name, its endpoint, the tools it exposes, and optional categories. The transport (such as SSE or streamable HTTP) is shown for each connector based on its endpoint. ## Edit or remove a connector [Section titled “Edit or remove a connector”](#edit-or-remove-a-connector) Each row’s **⋯** menu lets you **Edit** a connector or **Remove** it. Removing a connector also removes it as a target from any policy that referenced it. Click a connector to open its detail panel, which shows its transport, endpoint, categories, and the full list of tools it exposes, along with edit and remove actions and a YAML view. ## How connectors are used in policy [Section titled “How connectors are used in policy”](#how-connectors-are-used-in-policy) The tools listed here are the choices you get when restricting an agent in [Access Control](../access-control). Adding a connector rule for an MCP server restricts that server to the tools you list; tools of that server you do not list are blocked and hidden from the agent. Connectors without a rule stay fully allowed. Blocked tool calls appear in the [Activity Monitor](../activity-monitor). ## Next steps [Section titled “Next steps”](#next-steps) * [LLMs](../llms): the model providers agents can reach. * [Control agent access](../access-control): restrict which tools an agent may call. # LLMs The **LLMs** screen lists the model providers your agents can reach: the AI services Riptides recognizes, and the models each one offers. These are the providers you allow or restrict in an [access policy](../access-control), and the ones the [Activity Monitor](../activity-monitor) attributes model usage to. Open it from **Agentic → LLMs** in the sidebar. ## Built-in providers [Section titled “Built-in providers”](#built-in-providers) Riptides ships with the major LLM providers already defined, so you can start writing policy without registering anything. The built-in set includes: * **Anthropic** (`api.anthropic.com`): Claude Opus 4.8, Claude Sonnet 5, and Claude Haiku 4.5 models * **OpenAI** (`api.openai.com`): GPT-5, o4-mini, and GPT-4.1 models * **Mistral**, **DeepSeek**, **Cohere**, **Groq**, **Fireworks**, **Together**, **Perplexity**, **xAI (Grok)**, **OpenRouter**, and more Each provider is matched by its API endpoint (for example `api.anthropic.com:443`) and carries the list of models it offers. ## What you see [Section titled “What you see”](#what-you-see) ![The LLMs dashboard](/_astro/list.Bnh7r-md_ZAVj3b.webp) Three cards summarize the inventory: **Total providers**, **Active providers**, and **Total models**. The table lists each provider with: * **Status**: whether the provider is active. * **Provider**: its name and a summary of its models. * **Endpoint**: the API host it matches. * **Models**: how many models it offers. * **Last seen**: when traffic to it was last observed. Search by provider name. ## Add a provider [Section titled “Add a provider”](#add-a-provider) Use **Add provider** to register an LLM service that is not built in, for example a self-hosted or regional endpoint (such as an Azure-hosted OpenAI deployment). You give it a name, the endpoint it should match, and the models it offers. ## Edit or remove a provider [Section titled “Edit or remove a provider”](#edit-or-remove-a-provider) Each row’s **⋯** menu lets you **Edit** a provider (its endpoint and model list) or **Remove** it. Removing a provider also removes it as a target from any policy that referenced it. Click a provider to open its detail panel, which shows its status, endpoint, and the full list of registered models, along with edit and remove actions and a YAML view. ## How LLMs are used in policy [Section titled “How LLMs are used in policy”](#how-llms-are-used-in-policy) The models listed here are exactly the choices you get when restricting an agent in [Access Control](../access-control). Adding an LLM rule for a provider restricts that provider to the models you list; models of that provider you do not list are blocked, and the block shows up in the [Activity Monitor](../activity-monitor). Providers without a rule stay fully allowed. Keeping this inventory accurate, especially the model list on each provider, keeps the policy editor’s choices correct. ## Next steps [Section titled “Next steps”](#next-steps) * [Connectors](../connectors): the MCP servers and tools agents can use. * [Control agent access](../access-control): restrict which models an agent may call. # Agentic AI Overview Riptides secures and observes the AI agents running across your fleet. It shows you every model an agent talks to, every tool it invokes, and the person driving it, and it lets you set guardrails on which models and tools each agent is allowed to use, all with no changes to the agent or its configuration. AI agents are a new kind of workload: they act semi-autonomously, reach out to external LLM providers, and increasingly call real systems through tools (creating issues, querying databases, sending messages). That makes two questions urgent. What are my agents actually doing, and how do I keep them inside the lines? The Agentic AI features answer both. ## What you can do [Section titled “What you can do”](#what-you-can-do) The **Agentic** section of the console groups everything AI-related: * **[Activity Monitor](../activity-monitor)**: see every AI session as a graph: who ran the agent, which models it used, which tools it called, and anything that policy blocked. * **[LLMs](../llms)**: the model providers your agents can reach (Anthropic, OpenAI, Mistral, and others), and the models each one offers. * **[Connectors](../connectors)**: the MCP servers registered in your system (Linear, GitHub, Slack, and more) and the tools they expose. * **[Users](../users)**: the people who drive agents, so a policy can be scoped to a specific person. * **[Access Control](../access-control)**: the policies that decide which models and tools each agent may use, and who is allowed to drive it. ## Key concepts [Section titled “Key concepts”](#key-concepts) * **Agent**: in agentic AI terms, an application that uses a large language model to pursue goals and take actions, typically with some autonomy, often by calling tools or external systems, not only by answering a single prompt. In Riptides, each agent is represented as a [workload identity](../../concepts/workload-identity) whose AI traffic is observed and governed. (See **Current limitations** at the end of this page for what is written and tested today.) * **Session**: one agent run for a given workload and person. A session groups the back-and-forth **exchanges** the agent has with an LLM, including the tool calls it makes along the way. * **Exchange**: a single request/response turn with an LLM: the input, the model’s response, any tool calls it emitted, and token usage. * **LLM**: a model provider Riptides recognizes as an AI service (for example `anthropic-api`). Each provider exposes a set of **models**. * **Connector**: a registered MCP (Model Context Protocol) server, such as a Linear or GitHub integration. Each connector exposes a set of **tools** the agent can call. * **Tool call**: an agent’s invocation of a tool. This can be a **local** (built-in) tool the agent runs itself, such as reading a file or running a command, or a tool exposed by an MCP **connector** (for example `create_issue`) that is routed through the MCP server behind it. Both kinds show up in the Activity Monitor; only connector tools have a service behind them. * **User (actor)**: a person from the Users directory (from an identity provider or added manually). A policy can be scoped to a specific user so its rules apply only when that person is driving the agent. * **Access policy**: restrictions on models and tools for one agent, optionally scoped to one user (or to autonomous runs with no user). Default is allow; only providers and connectors you add a rule for are restricted. ## How it works [Section titled “How it works”](#how-it-works) Riptides runs on each host and gives every workload a cryptographic identity. When an AI agent calls an LLM or an MCP server, Riptides recognizes the call as AI traffic, applies the matching access policy when one governs that agent and driver (blocking models or tools that policy does not allow for that service), and records the interaction so it appears in the Activity Monitor. This happens transparently: the agent makes ordinary API calls, and Riptides observes and enforces in the middle, with no SDK, proxy configuration, or code changes in the agent. Because enforcement and observation are built on the same identity and connection security as the rest of Riptides, an agent is governed the moment it has an identity, and there is nothing to install into the agent itself. Policies are scoped: Autonomous Operation covers unattended runs, and each user policy covers only that person. A human without a user policy is not limited by Autonomous Operation. For each policy, traffic is allowed by default and only the providers and connectors you restrict are narrowed. For the underlying platform, see [Platform Architecture](../../concepts/architecture) and [Connection Security](../../concepts/connections). ## Next steps [Section titled “Next steps”](#next-steps) Prerequisite An agent only appears here once it exists as a workload identity with traffic inspection turned on. Create one for the agent’s process (with **TLS intercept** on) on the [Identities](../../console/identities) screen. * [Monitor AI activity](../activity-monitor): read a session and trace what an agent did. * [Control agent access](../access-control): create a policy to restrict models, tools, and users. * [Manage LLMs](../llms) and [Connectors](../connectors): review the providers and MCP servers agents can reach. Current limitations The Activity Monitor and Access Control are written and tested for **Claude Code** and **Codex**. They can also work with other LLM-based applications, but you may see gaps, for example incomplete sessions in the Activity Monitor, or policies that do not cover every model or tool call the way they do for those agents. # Users The **Users** screen lists the people who drive agents in Riptides. These are the humans you scope a policy to when you want a rule to apply only to a specific person. In an [access policy](../access-control), a user is the **actor**: the human on whose behalf an agent is acting. Open it from **Agentic → Users** in the sidebar. ## Where users come from [Section titled “Where users come from”](#where-users-come-from) Users can come from an identity provider you connect to Riptides (for example GitHub, Google, Microsoft Entra, or a generic OIDC provider). When someone signs in, they appear here with the details their provider supplies. They can also be added manually. To set up identity-provider sign-in, see [OIDC and Identity Provider Setup](../../guides/oidc-setup). Each user carries a **SPIFFE ID**, a stable identity Riptides assigns them. That identity is the link that lets a policy target one specific person: when an agent runs on someone’s behalf, Riptides attributes the activity to their identity, and a user-scoped policy matches on it. ## What you see [Section titled “What you see”](#what-you-see) Four cards summarize the directory: **Total users**, **Active (30d)**, **With SPIFFE ID**, and **With groups**. The table lists each user with: * **User**: name and email. * **Provider**: the identity provider they signed in through (used as a filter), when applicable. * **SPIFFE ID**: the identity assigned to them, or a dash if none. * **Last login**: when they last signed in. Search by name or email, and filter by provider. ## How users are used in policy [Section titled “How users are used in policy”](#how-users-are-used-in-policy) A policy can either govern an agent **regardless of who is driving it** or apply **only when a specific user is driving it**. The second case is exactly where this screen matters: when you restrict a workload to specific users in [Access Control](../access-control), you pick those people from this list. The [Activity Monitor](../activity-monitor) also shows the user behind each session, so you can see who ran which agent. ## Next steps [Section titled “Next steps”](#next-steps) * [Control agent access](../access-control): restrict an agent to specific users. * [Monitor AI activity](../activity-monitor): see which user drove each session. # AI Agent Governance An AI agent is a workload like any other: a process with a [workload identity](../workload-identity), secured the same way as a service or a CI job. What’s different is what it does with that identity. A service calls a fixed set of destinations in a predictable order; an agent decides at runtime what to call based on an LLM’s output, so the same agent can take a different path through your infrastructure on every run. Identity and network policy alone answer “is this workload allowed to reach this destination,” but they don’t answer the questions that actually matter for an agent: which model did it use, which tool did it call, and who was driving it when it did. Riptides extends the same kernel-level enforcement it uses for mTLS and credential injection to answer those questions, without adding an SDK, a proxy, or any change to the agent’s code or configuration. ## The Building Blocks [Section titled “The Building Blocks”](#the-building-blocks) * **Agent**: not a new resource type. An agent is a [WorkloadIdentity](../../reference/workloadidentity) like any other, with TLS interception turned on so the kernel can inspect its HTTP traffic to recognize LLM and MCP calls. * **Actor**: the human on whose behalf the agent is acting. Riptides resolves this from the credentials the agent itself presents to the LLM provider, so every action carries two identities: the agent’s workload identity and the actor’s. This composite identity is what lets a policy apply only when a specific person is driving, and what lets an audit trail answer “who” as well as “what.” * **Session** and **exchange**: a session is one agent run; an exchange is one request/response turn within it, including any tool calls the agent made along the way. Sessions and exchanges are how agent activity is organized for observability. * **Tool call**: an agent invoking a tool, which is either **local** (a built-in action the agent runs itself, such as reading a file or running a command) or routed through an MCP **connector**. Both are observed the same way * **LLMs** and **connectors**: the vocabulary a policy is written against. An LLM is a model provider (Anthropic, OpenAI, and others) and the models it offers; a connector is an MCP server (Linear, GitHub, Slack, and others) and the tools it exposes. * **Access policy**: the rule set that governs one agent, applied either to its autonomous runs or to a specific actor driving it. ## How It Works [Section titled “How It Works”](#how-it-works) Because an agent’s traffic is ordinary HTTPS, Riptides recognizes it the same way it recognizes any traffic it intercepts: by inspecting the connection at the kernel, not by requiring the agent to route through a separate proxy or gateway. Known LLM and MCP endpoints are classified as AI traffic; the driving actor is resolved from the agent’s own provider credentials; and the applicable access policy, if any, is evaluated before the request leaves the host. A call that policy doesn’t allow is blocked at that point, not after it reaches the provider. Every exchange, including anything blocked, is recorded for the Activity Monitor. ## Default Allow, Restrict by Exception [Section titled “Default Allow, Restrict by Exception”](#default-allow-restrict-by-exception) An agent with no policy, or an empty one, has full access. Adding a rule for a specific provider or connector narrows only that one: the models or tools you leave on for it are permitted, everything else on that same provider or connector is blocked, and every other provider or connector the policy doesn’t mention stays fully allowed. This means adopting governance is incremental: you can restrict one high-risk connector today without having to enumerate everything else an agent is allowed to touch. Policy also distinguishes *who* is driving the agent. An autonomous-operation policy governs unattended runs; a per-user policy governs one specific person driving the agent. There’s no fallback between them: a human with no policy of their own gets full access even if the agent’s autonomous policy is locked down. This lets you, for example, run an agent unattended under a strict allowlist while giving a trusted engineer broader access when they’re the one behind the wheel. ## Why Not a Gateway or Prompt Guardrails [Section titled “Why Not a Gateway or Prompt Guardrails”](#why-not-a-gateway-or-prompt-guardrails) ### AI and MCP Gateways [Section titled “AI and MCP Gateways”](#ai-and-mcp-gateways) A gateway, whether it fronts LLM calls or MCP tool calls, only governs what’s actually routed through it. An agent that makes a direct connection, or reaches anything outside the gateway’s configured path, bypasses it entirely, so coverage depends on every agent being correctly wired to the gateway rather than on anything the platform itself guarantees. Local tool calls make this worse than a bypass risk: reading a file or running a command never goes over the network at all, so no gateway, however well every agent is configured, could ever see it. A gateway is also a piece of infrastructure you now have to scale and keep available: it sits in the request path of every governed call, so its capacity has to grow with your agent traffic instead of with your actual node count. Riptides enforces where the agent already runs, at the kernel, so there’s nothing centralized to add capacity to, and local tool calls are covered the same way connector calls are. Because every connection is inspected there regardless of destination, you get a complete audit trail of every LLM call, MCP call, and local tool call an agent made, not just the ones that happened to be routed through a gateway. ### Prompt Guardrails [Section titled “Prompt Guardrails”](#prompt-guardrails) Prompt guardrails try to stop an agent from *being told* to do something unsafe, but they operate on the prompt, not the connection. A credential sitting in the agent’s memory, or a tool call to an unapproved endpoint, doesn’t have to go through a prompt to happen, so it’s outside what a guardrail can see or stop. Because Riptides enforces at the node, every outbound connection the agent makes is covered, regardless of which framework, library, or tool call initiated it, and regardless of what the agent’s own reasoning decided to do. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Agentic AI Overview](../../agentic/overview): the console features built on this model. * [Monitor AI activity](../../agentic/activity-monitor) and [control agent access](../../agentic/access-control): the day-to-day workflows. * [Guardrails for an AI Agent](../../examples/agentic-guardrails): a worked example end to end. # Platform Architecture Riptides is an identity-first workload security platform that enforces authentication, authorization, and credential management at the kernel level. The platform consists of three components: a centralized control plane, a per-node daemon, and a Linux kernel module. Together, they provide transparent mutual TLS, workload identity, and credential injection without application changes. ## Components [Section titled “Components”](#components) ### Control Plane [Section titled “Control Plane”](#control-plane) The control plane is the central management layer. It defines and distributes identity policies, issues certificates, and acts as the trust anchor for the entire platform. The control plane includes: * **API server** — A Kubernetes-style API server that exposes Custom Resource Definitions (CRDs) for all Riptides resources: workload identities, services, credential sources, credential bindings, verifiers, and federation configuration. Operators interact with it using [`riptides-cli ctl`](../../deployment/cli). * **Controllers** — Reconcile declared policy into concrete artifacts (certificates, access rules, credential bindings) that the enforcement layer consumes. * **OIDC provider** — Handles user authentication for the control plane UI and API. External identity providers (GitHub, Google, etc.) authenticate human users via OIDC, and Riptides manages sessions and access control. * **gRPC server** — Serves policy, certificate, and credential updates to daemons. Daemons maintain a persistent gRPC connection and receive updates in real time. * **Tunnel server** — Provides a persistent, daemon-initiated channel the control plane uses to retrieve telemetry from daemons in a pull model. * **Front proxy** — TLS-terminates inbound API and gRPC connections and routes them to the appropriate backend. The control plane can be deployed as a managed service, a dedicated instance, or on-premises. ### Daemon [Section titled “Daemon”](#daemon) The daemon runs on every node that hosts workloads. In Kubernetes environments it runs as a DaemonSet; on bare-metal or VM hosts it runs as a standalone binary. The daemon is responsible for: * **Node attestation** — On startup, the daemon authenticates to the control plane using a configured Verifier (JoinToken, AWS Instance Identity Document, GCP Instance Identity Token, and others). This establishes the node’s identity. * **Metadata collection** — Gathers host and workload metadata from the environment: Kubernetes pod labels, namespaces, and container names; AWS instance identity; OS information; network interfaces. This metadata drives workload attestation. * **Policy synchronization** — Receives identity, service, and credential policies from the control plane and loads them into the kernel module. * **Certificate operations** — Acts as a local certificate authority or forwards certificate signing requests to the control plane. Signs CSRs generated by the kernel module for workload SVIDs. * **Kernel communication** — Communicates with the kernel module through the `/dev/riptides` character device using Protocol Buffer messages. ### Kernel Module [Section titled “Kernel Module”](#kernel-module) The kernel module is a Linux kernel module (not eBPF) that enforces security policy at the socket level. It intercepts TCP connections transparently and applies identity-based authentication and authorization without any application changes. Key capabilities: * **TLS/mTLS termination** — Performs full TLS 1.3, quantum-ready handshakes in kernel space. When kernel TLS (kTLS) is available, symmetric cryptographic operations are offloaded to kTLS for performance, with an in-kernel TLS implementation as a fallback. * **Private key protection** — Workload private keys are generated and stored in kernel memory. They are never accessible from user space, eliminating an entire class of key-theft attacks. * **Policy enforcement** — Evaluates access control rules at connection time. Connections that violate policy are dropped before any application code executes. * **Credential injection** — Intercepts outbound HTTP requests and rewrites headers on the wire (for example, adding `Authorization` headers or AWS SigV4 signatures) before packets leave the machine. The application never handles the credential directly. * **Transparent operation** — Applications connect using ordinary plaintext TCP sockets. The kernel module intercepts at the socket level and upgrades connections to TLS or mTLS as policy dictates. ## Trust Domains and SPIFFE [Section titled “Trust Domains and SPIFFE”](#trust-domains-and-spiffe) Riptides uses the SPIFFE (Secure Production Identity Framework for Everyone) standard for workload identity. Every deployment operates within a **trust domain** (for example, `example.com`). Within a trust domain, each workload receives a **SPIFFE ID** of the form: ```plaintext spiffe:/// ``` Workloads prove their identity using **SVIDs** (SPIFFE Verifiable Identity Documents), which are X.509 certificates or JWT tokens containing the workload’s SPIFFE ID. ## Data Flow [Section titled “Data Flow”](#data-flow) The following describes how policy moves from declaration to enforcement: 1. **Declare** — An operator creates or updates identity, service, and credential policies on the control plane using CRDs. 2. **Distribute** — The control plane reconciles the policies and pushes the resulting configuration (certificates, access rules, credential bindings) to daemons over gRPC. 3. **Load** — Each daemon writes the configuration to the kernel module through the `/dev/riptides` character device. 4. **Enforce** — The kernel module applies the configuration immediately. New and existing connections are evaluated against the loaded policies. TLS handshakes use the provisioned certificates, and credential injection rules take effect on matching outbound requests. This push-based architecture means policy changes propagate to all nodes without requiring workload restarts or redeployments. # Connection Security Riptides secures workload connections at the kernel level. Every TCP connection is evaluated against identity-based policies, and TLS is applied transparently without application changes. This document covers TLS modes, transparent mTLS, TLS handling strategies, and access control policies. ## TLS Modes [Section titled “TLS Modes”](#tls-modes) Each connection between a workload and a service operates in one of three TLS modes, configured on the WorkloadIdentity’s ingress or egress rules: ### MUTUAL [Section titled “MUTUAL”](#mutual) Both sides of the connection present X.509 SVIDs and verify each other’s identity. This is the strongest mode and the recommended default for internal service-to-service communication. Riptides supports TLS 1.2 and TLS 1.3. TLS 1.2 is provided for compatibility with external services; for internal communication between Riptides-managed processes using `connection.tls.mode: MUTUAL`, the mTLS tunnel is always negotiated with TLS 1.3. With TLS 1.3, Riptides also offers PQC mTLS for internal service-to-service communication. The kernel module performs the full mTLS handshake transparently. The application sends and receives plaintext; the kernel handles certificate exchange, verification, and encryption. ### SIMPLE [Section titled “SIMPLE”](#simple) Only the server side presents a certificate. The client verifies the server’s identity but does not present its own. Use this for connections to external services that require server authentication but do not support client certificates. ### PERMISSIVE [Section titled “PERMISSIVE”](#permissive) Accepts both plaintext and TLS connections on the same port. This mode is useful during migration: workloads that have been enrolled in Riptides will connect over mTLS, while workloads that have not yet been enrolled can still connect over plaintext. Permissive mode is intended as a transitional step, not a permanent configuration. Once all connecting workloads are enrolled, switch to MUTUAL. ## Transparent mTLS [Section titled “Transparent mTLS”](#transparent-mtls) Riptides provides transparent mTLS: applications make ordinary plaintext TCP connections, and the kernel module automatically upgrades them to mutual TLS. This works because the kernel module intercepts connections at the socket level. When a workload opens a connection to a destination that matches an egress rule with `connection.tls.mode: MUTUAL`, the kernel module: 1. Intercepts the TCP handshake. 2. Performs a TLS handshake with the remote side, presenting the workload’s X509 SVID. 3. Verifies the remote side’s SVID against the loaded trust bundle. 4. Encrypts all subsequent traffic using the negotiated session keys. The application sees a normal TCP connection. It does not need TLS libraries, certificate files, or any awareness that encryption is happening. On the receiving side, the kernel module on the destination node terminates the mTLS connection, verifies the caller’s SVID, evaluates ingress policies, and delivers plaintext to the application. ## TLS Handling Strategies [Section titled “TLS Handling Strategies”](#tls-handling-strategies) The kernel module handles connections differently depending on whether the application is already using TLS: ### TLS Intercept [Section titled “TLS Intercept”](#tls-intercept) When `connection.tls.intercept` is enabled, the kernel module terminates the application’s TLS connection locally, inspects and modifies the HTTP payload, then establishes a new TLS connection to the destination. The daemon evaluates the connection context and, if a credential injection is required, sets a reference in the evaluation context. The kernel checks for this reference and injects the credential before forwarding the request. ### TLS Pass-Through [Section titled “TLS Pass-Through”](#tls-pass-through) When a connection is already encrypted and no credential injection or policy evaluation at the HTTP layer is needed, the kernel module can wrap the existing encrypted connection in an additional Riptides mTLS layer for identity and policy enforcement. The original TLS payload is not re-encrypted; it passes through intact inside the outer mTLS tunnel. The kernel module can inspect TLS handshake metadata (such as the ClientHello and ALPN extensions) to make routing and policy decisions without decrypting the payload. ## Ingress and Egress Policies [Section titled “Ingress and Egress Policies”](#ingress-and-egress-policies) Connection policies are defined on WorkloadIdentity resources and control both inbound and outbound connections. ### Egress [Section titled “Egress”](#egress) Egress rules specify which services a workload is allowed to connect to and under what conditions: ```yaml spec: egress: - selectors: - tier: backend connection: tls: mode: MUTUAL allowedSPIFFEIDs: - "spiffe://example.com/orders-service" - "spiffe://example.com/inventory-service" ``` * **egress\[].selectors** - Matches destination services by label. See [Services](../services). * **egress\[].connection.tls.mode** - The TLS mode for the connection (MUTUAL, SIMPLE, or PERMISSIVE). * **egress\[].allowedSPIFFEIDs** - Restricts which SPIFFE IDs the remote service must present. If specified, the connection is only established if the remote side’s SVID matches one of the listed identities. ### Ingress [Section titled “Ingress”](#ingress) Ingress rules specify which workloads are allowed to connect to this workload: ```yaml spec: ingress: - port: 8000 allowedSPIFFEIDs: - "spiffe://example.com/frontend" - "spiffe://example.com/monitoring" ``` * **ingress\[].port** - The port on which this rule applies. * **ingress\[].allowedSPIFFEIDs** - Only workloads presenting one of the listed SPIFFE IDs are allowed to connect. Connections from any other identity are rejected at the TLS handshake. If no `allowedSPIFFEIDs` are specified, any workload within the trust domain that satisfies the TLS mode requirement can connect. #### Restricting unauthenticated access by HTTP path [Section titled “Restricting unauthenticated access by HTTP path”](#restricting-unauthenticated-access-by-http-path) On a `PERMISSIVE` port, an ingress rule can additionally limit which HTTP request paths an **unauthenticated** caller (a plaintext client presenting no SPIFFE identity) may reach, using `httpRequestPath`: ```yaml spec: ingress: - port: 8080 connection: protocol: HTTP1 tls: mode: PERMISSIVE httpRequestPath: - /healthz - /readyz ``` The port still accepts the plaintext connection, but the kernel module inspects each request and forwards only the listed paths. Any other request - or anything that is not a recognizable HTTP/1 request - is reset, the same outcome a `MUTUAL` port gives an unauthenticated caller. Clients presenting a valid mTLS identity are not restricted and reach every path. This is the typical pattern for health and readiness probes. A probe such as `GET /healthz` from a kubelet carries no workload identity, so a `MUTUAL` port would reject it; allowlisting just the probe paths lets it through while every other path on the port still requires identity. `httpRequestPath` requires `connection.protocol: HTTP1` and matches paths exactly, ignoring the query string. ## Connection Flow Example [Section titled “Connection Flow Example”](#connection-flow-example) Consider a frontend workload connecting to an internal orders service: 1. The frontend application opens a plaintext TCP connection to `orders-service.internal.example.com:8080`. 2. The kernel module intercepts the connection and negotiates with the remote side to establish whether the peer is also a Riptides-managed workload. 3. The kernel module initiates an mTLS handshake, presenting the frontend’s SVID (`spiffe://example.com/frontend`) and verifying the peer’s SPIFFE SAN. 4. The kernel module on the orders service node terminates the mTLS connection and checks the ingress policy. The frontend’s SPIFFE ID is in the `allowedSPIFFEIDs` list. 5. The connection is established. Both sides exchange plaintext with their local kernel modules, while the network carries encrypted, mutually authenticated traffic. # Credentials Not all services support SPIFFE-based mutual TLS. External APIs, cloud provider services, and legacy systems typically require bearer tokens, API keys, or signed requests. Riptides bridges this gap with a credential pipeline that binds secrets to attested workload identities and delivers them without exposing credentials to application code. ## How Credentials Work [Section titled “How Credentials Work”](#how-credentials-work) Credential management in Riptides is built around two ideas: * **Where a credential comes from.** You describe the integration with an external credential provider - how to authenticate to it and how to retrieve or generate a credential. Each provider type handles its own specifics. * **Which workloads receive it, and how.** You connect a credential to one or more workloads and choose how it should be delivered to each. Keeping these separate means a single credential definition can be reused across many workloads with different delivery mechanisms, and provider configuration is managed independently from workload policy. ## JWT-SVID Credential Federation [Section titled “JWT-SVID Credential Federation”](#jwt-svid-credential-federation) Riptides issues JWT-SVIDs for attested workloads - signed JWTs containing the workload’s SPIFFE ID and other claims. Cloud providers support workload identity federation, allowing Riptides to exchange a workload’s JWT-SVID for temporary cloud credentials without any long-lived keys: 1. The control plane presents the workload’s JWT-SVID to the cloud provider’s STS endpoint. 2. The cloud provider validates the JWT-SVID and issues temporary, scoped credentials. 3. The control plane pushes those credentials to the daemon, which loads them into the kernel for injection. This is the mechanism behind the AWS, GCP, and Azure provider types. For step-by-step setup, see the [Secretless AWS Access](../guides/secretless-aws) and [Secretless GCP Access](../guides/secretless-gcp) guides. ## Provider Types [Section titled “Provider Types”](#provider-types) | Type | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------- | | **AWS** | Exchanges the workload’s JWT-SVID with AWS STS via `AssumeRoleWithWebIdentity` to obtain temporary IAM credentials. | | **GCP** | Exchanges a workload identity token for GCP access credentials via Workload Identity Federation. | | **Azure** | Exchanges a workload identity token for Azure access credentials via federated identity credentials. | | **Vault/OpenBao** | Authenticates to HashiCorp Vault or OpenBao and retrieves a secret or dynamic credential. | | **Static** | Stores a static, pre-provisioned secret such as a long-lived API key or token. | | **JWT** | Issues a signed JWT with custom claims, useful for authenticating to APIs that accept JWT bearer tokens. | | **OAuth2** | Performs an OAuth2 client credentials or authorization code flow to obtain an access token. | | **OCI** | Retrieves credentials for OCI-compliant container registries. | ## Delivery Mechanisms [Section titled “Delivery Mechanisms”](#delivery-mechanisms) Once a credential is obtained, Riptides delivers it to the workload through one of two mechanisms: ### Injection (On-the-Wire Header Rewriting) [Section titled “Injection (On-the-Wire Header Rewriting)”](#injection-on-the-wire-header-rewriting) The kernel module intercepts outbound HTTP requests and adds or rewrites headers before the packets leave the machine. The application makes a plain HTTP request; by the time it reaches the destination, the kernel has injected the required credentials. Examples of injected credentials: * **Bearer tokens** - The kernel adds an `Authorization: Bearer ` header to outgoing requests. * **AWS SigV4** - The kernel computes the AWS Signature Version 4 signing process and adds the `Authorization`, `X-Amz-Date`, `X-Amz-Security-Token`, and other required headers. * **Custom headers** - Arbitrary header injection for APIs that expect credentials in non-standard headers. This is the recommended delivery mechanism. The credential exists only in kernel memory and is written directly into the network buffer. It is never exposed to user-space processes, environment variables, or the filesystem. ### Sysfs (Secure File-Based Delivery) [Section titled “Sysfs (Secure File-Based Delivery)”](#sysfs-secure-file-based-delivery) For applications that read credentials from the filesystem (such as SDK-based credential providers or legacy applications), Riptides exposes credential files through the kernel module’s sysfs interface: ```plaintext /sys/module/riptides/credentials//token ``` The kernel module owns these files and controls access. Credentials are refreshed automatically as they rotate. Applications that watch the filesystem or re-read on each use will always get a current credential. ## Credential Lifecycle [Section titled “Credential Lifecycle”](#credential-lifecycle) Credentials managed by Riptides are short-lived and automatically refreshed: 1. **Initial fetch** - When a credential is bound to a workload, the control plane obtains it from the configured provider and pushes it to the daemon. 2. **Delivery** - The daemon loads the credential into the kernel module for injection, or exposes it via sysfs. 3. **Rotation** - Before the credential expires, the control plane fetches a new one and pushes it to the daemon, which updates the kernel module. For injection, the kernel immediately uses the new credential on subsequent requests. For sysfs, the file contents are updated atomically. 4. **Revocation** - When a binding is removed or the workload no longer matches its identity selectors, the credential is deleted from the kernel module. Injection stops, and sysfs files are removed. The entire lifecycle is automatic. Operators define where credentials come from and which workloads receive them; the platform handles retrieval, delivery, rotation, and cleanup. ## Example [Section titled “Example”](#example) An application that needs to access an AWS S3 bucket: ```yaml apiVersion: core.riptides.io/v1alpha1 kind: CredentialSource metadata: name: s3-access namespace: riptides-system spec: aws: roleArn: "arn:aws:iam:::role/" awsRegion: "us-east-1" --- apiVersion: core.riptides.io/v1alpha1 kind: CredentialBinding metadata: name: frontend-s3 namespace: riptides-system spec: workloadID: frontend/workload credentialSource: s3-access propagation: injection: selectors: - app: frontend service: s3 ``` With this configuration: 1. The control plane issues an OIDC token for the frontend workload. 2. AWS STS exchanges the token for temporary credentials scoped to the specified IAM role. 3. The kernel module signs outbound S3 requests with SigV4 headers. 4. The frontend application calls S3 using plain HTTP. It never sees or handles AWS credentials. # Trust Domain Federation Trust domain federation enables workloads in separate Riptides deployments to authenticate to each other across trust boundaries. Each Riptides deployment operates its own trust domain with its own CA. Federation is the mechanism by which two trust domains can validate each other’s SVIDs without merging into a single deployment. ## Trust Bundles [Section titled “Trust Bundles”](#trust-bundles) A trust bundle contains the signing CA certificates for a trust domain. The control plane distributes the local trust bundle to all daemons. When a workload receives a connection from a remote workload, the kernel module validates the remote SVID against the trust bundle for that domain. To accept connections from a foreign trust domain, that domain’s trust bundle must be loaded into the local control plane and distributed to daemons. ## Cross-Domain Federation [Section titled “Cross-Domain Federation”](#cross-domain-federation) Multiple Riptides deployments, each operating their own trust domain, can federate by exchanging trust bundles. This enables: * **Multi-cluster communication** - Workloads in different Kubernetes clusters (each with its own trust domain) can authenticate to each other via mTLS using their respective SPIFFE IDs. * **Multi-cloud communication** - Workloads running in different cloud providers, managed by separate control planes, can establish trusted connections. * **Partner and third-party access** - Organizations can share trust bundles with partners, allowing cross-organization workload authentication without sharing credentials. ### How It Works [Section titled “How It Works”](#how-it-works) 1. Each control plane publishes its trust bundle (the signing CA certificates for its trust domain). 2. Administrators configure each control plane to trust the other domain’s bundle. 3. The control planes distribute the foreign trust bundles to their daemons. 4. The kernel module loads all trust bundles and can verify SVIDs from any trusted domain. When a workload in `example.com` connects to a workload in `partner.com`, the kernel modules on both sides verify the remote SVID against the appropriate trust bundle. Ingress and egress policies can reference SPIFFE IDs from foreign trust domains in their `allowedSPIFFEIDs` lists. ### Example [Section titled “Example”](#example) ```yaml apiVersion: core.riptides.io/v1alpha1 kind: TrustBundle metadata: name: partner-trust namespace: riptides-system spec: trustDomain: "partner.com" bundle: | -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- ``` With this trust bundle loaded, workloads in the local trust domain can verify and accept connections from workloads presenting SVIDs issued by `partner.com`. # Kernel Module Riptides uses a Linux kernel module to enforce security policies transparently at the socket level. The kernel module handles TLS/mTLS termination, credential injection, and policy enforcement - all without requiring any changes to application code. ## Why a Kernel Module? [Section titled “Why a Kernel Module?”](#why-a-kernel-module) Riptides chose a kernel module over eBPF for several technical reasons. eBPF programs are sandboxed and limited in what they can do - they cannot generate cryptographic keys, perform full TLS handshakes, inject credentials into HTTP streams, or create sysfs filesystem entries. These are all core capabilities that Riptides requires: | Capability | eBPF | Kernel Module | | ---------------------------------------- | ------- | ------------- | | Generate private keys | No | Yes | | Full TLS handshakes | No | Yes | | Credential injection (header rewriting) | No | Yes | | Sysfs filesystem for credential delivery | No | Yes | | Policy enforcement at socket level | Limited | Yes | | Private keys isolated from userspace | No | Yes | The kernel module approach means private keys are generated and stored exclusively in kernel memory - they are never accessible from userspace, even by the application they belong to. ## TLS in Kernel Space [Section titled “TLS in Kernel Space”](#tls-in-kernel-space) Riptides runs a compact TLS library in kernel space that provides: * TLS 1.3, with TLS 1.2 supported for interoperability with legacy peers * Modern AEAD cipher suites: AES-256-GCM, AES-128-GCM, and ChaCha20-Poly1305 * X.509 certificate handling and SPIFFE identity verification * Small memory footprint suitable for kernel context The in-kernel TLS implementation handles the initial handshake (key exchange, certificate verification) and can process the ongoing encrypted stream when kTLS offload is not available. For transparent mTLS between Riptides-managed workloads, the internal connection is always established with TLS 1.3. ## Quantum Safety [Section titled “Quantum Safety”](#quantum-safety) Riptides protects the confidentiality of mesh traffic against quantum attack today, including the *harvest-now, decrypt-later* threat where an adversary records encrypted traffic now to decrypt once a quantum computer exists. Both layers that establish and protect the data stream - the key exchange and the symmetric cipher - are quantum-resistant. Authentication (certificate signatures) remains classical for now. ### Key exchange - post-quantum hybrid [Section titled “Key exchange - post-quantum hybrid”](#key-exchange---post-quantum-hybrid) Session keys are established with a **post-quantum hybrid key exchange**. The `X25519MLKEM768` group combines **ML-KEM-768** (FIPS 203, the standardized successor to Kyber) with classical **X25519**: the shared secret is derived from both an ML-KEM encapsulation and an X25519 Diffie-Hellman exchange. The result is at least as strong as X25519 and adds post-quantum protection, so recorded handshakes cannot be broken by a future quantum computer. ML-KEM runs entirely in kernel space as part of the in-kernel TLS 1.3 handshake, using a kernel-native FIPS 203 implementation (the SHA-3/SHAKE primitives it depends on are built into the module). Because the handshake always runs in the module - kTLS only offloads the symmetric layer afterward - the post-quantum key exchange applies whether or not kTLS is in use. The exchange degrades gracefully. For transparent mTLS between Riptides workloads, both ends support the hybrid group and negotiate it directly. For other destinations - including TLS intercept toward external servers - the classical groups remain on offer, so a peer that does not support the hybrid falls back to X25519 automatically: post-quantum wherever the peer allows it, with no loss of connectivity where it does not. ### Symmetric layer - quantum-resistant [Section titled “Symmetric layer - quantum-resistant”](#symmetric-layer---quantum-resistant) Bulk traffic is protected by 256-bit AEAD cipher suites - **AES-256-GCM** and **ChaCha20-Poly1305**, which the module offers ahead of their 128-bit counterparts, so a mesh connection negotiates one of them. Symmetric ciphers of this strength are considered quantum-resistant: the best known quantum attack (Grover’s algorithm) only halves the effective key length, leaving AES-256 and ChaCha20 at roughly 128 bits of post-quantum security. AES-128-GCM stays on offer for peers that require it - relevant only for TLS intercept toward external servers, where the remote end picks the suite. ### Authentication - classical today [Section titled “Authentication - classical today”](#authentication---classical-today) Workload identities are still authenticated with classical signatures - ECDSA (P-256/P-384) or RSA - over SPIFFE X.509 certificates. ML-KEM protects the key exchange (confidentiality), not authentication, so this remains a classical primitive. It is a lower-urgency gap than key exchange: forging a signature requires an active attack at connection time rather than passive recording, so it is not exposed to harvest-now-decrypt-later. Post-quantum signatures (such as ML-DSA / FIPS 204) are a future step. Note The negotiated key-exchange group is visible per connection in `/proc/riptides/connections` as `kex_group` - `X25519MLKEM768` indicates the post-quantum hybrid handshake, `X25519` indicates a classical fallback. ## kTLS Integration [Section titled “kTLS Integration”](#ktls-integration) When the kernel’s built-in TLS support ([kTLS](https://docs.kernel.org/networking/tls-offload.html)) is available, Riptides offloads symmetric encryption to it for better performance. The flow is: 1. The in-kernel TLS implementation performs the handshake (asymmetric crypto, certificate exchange) 2. Once the session is established, symmetric keys are handed to kTLS 3. kTLS handles bulk data encryption/decryption in the kernel’s networking stack If kTLS is not available or not applicable for a particular connection, the in-kernel TLS implementation handles the entire TLS session as a fallback. ## Transparent Operation [Section titled “Transparent Operation”](#transparent-operation) Applications connect to network services using standard TCP sockets - no TLS libraries, no certificate management code, no authentication logic. The kernel module intercepts connections at the socket level and: 1. **Outbound connections**: Initiates a TLS/mTLS handshake with the destination and optionally injects credentials into the HTTP stream based on the evaluation context set by the daemon 2. **Inbound connections**: Terminates TLS, verifies the client certificate against allowedSPIFFEIDs, and forwards plaintext to the application From the application’s perspective, it sends and receives plaintext. From the network’s perspective, all traffic is encrypted and authenticated. ## TLS Termination Modes [Section titled “TLS Termination Modes”](#tls-termination-modes) The module supports several TLS handling strategies depending on the connection and policy: * **Transparent mTLS**: Plaintext connections are automatically upgraded to mTLS. Both endpoints present SPIFFE certificates. * **TLS intercept**: The module terminates an existing TLS connection and re-establishes it, allowing credential injection into the HTTP stream between the two TLS sessions. Because the module presents a Riptides-issued certificate to the application, any userspace library or SDK that performs its own TLS verification must be configured to trust the Riptides CA. See [Trusting the Riptides CA](#trusting-the-riptides-ca-for-tls-intercept) below. * **TLS pass-through**: For connections that are already encrypted, the module can wrap them in an additional Riptides mTLS layer for identity verification without modifying the original payload. The original TLS is not double-encrypted - the Riptides layer provides identity and policy enforcement only. ## Trusting the Riptides CA for TLS Intercept [Section titled “Trusting the Riptides CA for TLS Intercept”](#trusting-the-riptides-ca-for-tls-intercept) When `tls.intercept: true` is configured on an egress rule, the kernel module terminates the application’s outbound TLS connection and re-originates a new one to the destination. The application sees a Riptides-issued certificate instead of the destination’s original certificate. Any userspace library or SDK that performs its own certificate verification will reject this certificate unless configured to trust the Riptides CA. The CA bundle is available at: ```plaintext /sys/module/riptides/certs/ca-certificates.crt ``` Configure your application’s TLS trust store to include this file. Common environment variables: | SDK / Library | Environment Variable | | ----------------- | -------------------------------------------------------------------- | | AWS SDK | `AWS_CA_BUNDLE=/sys/module/riptides/certs/ca-certificates.crt` | | Python `requests` | `REQUESTS_CA_BUNDLE=/sys/module/riptides/certs/ca-certificates.crt` | | Node.js | `NODE_EXTRA_CA_CERTS=/sys/module/riptides/certs/ca-certificates.crt` | | Go (net/http) | `SSL_CERT_FILE=/sys/module/riptides/certs/ca-certificates.crt` | | Generic OpenSSL | `SSL_CERT_FILE=/sys/module/riptides/certs/ca-certificates.crt` | | cURL | `CURL_CA_BUNDLE=/sys/module/riptides/certs/ca-certificates.crt` | Alternatively, you can append the Riptides CA to your system’s existing CA bundle if you prefer a single trust store. ## Credential Injection [Section titled “Credential Injection”](#credential-injection) One of the kernel module’s unique capabilities is on-the-wire credential injection. When a CredentialBinding is configured with injection propagation, the module: 1. Intercepts outbound HTTP requests at the socket level 2. Adds or rewrites HTTP headers (e.g., `Authorization: Bearer `, AWS SigV4 signature headers) 3. Forwards the modified request to the destination The application makes a plain HTTP request without any authentication headers. The daemon evaluates the connection context and determines which credentials to inject; the kernel checks the evaluation context and adds them transparently. This means credentials are never exposed in application memory. ## Sysfs Based Secure Credential Delivery [Section titled “Sysfs Based Secure Credential Delivery”](#sysfs-based-secure-credential-delivery) For applications that need to read credentials from the filesystem (e.g., GCP Application Default Credentials), the kernel module provides credentials via sysfs: ```plaintext /sys/module/riptides/credentials///token.jwt ``` Applications can read credential files from this path. The kernel module owns these files, controls access, and manages credential rotation automatically. Reading the file always returns a current, valid credential. ## Daemon Communication [Section titled “Daemon Communication”](#daemon-communication) The kernel module communicates with the userspace daemon via the `/dev/riptides` character device. The daemon uses Protocol Buffers over this interface to: * Push identity configurations (certificates, private keys, SPIFFE IDs) * Push service definitions (addresses, ports, labels) * Push credential bindings and policies * Receive health and status information The daemon acts as the bridge between the control plane (which manages policy centrally) and the kernel module (which enforces policy locally). ## Diagnostics [Section titled “Diagnostics”](#diagnostics) The module exposes diagnostic information through procfs and sysfs: | Endpoint | Description | | ----------------------------------- | ----------------------------------- | | `/sys/module/riptides/health` | Module health status | | `/proc/riptides/certificates` | Loaded certificates (JSON) | | `/proc/riptides/connections` | Active connections (JSON) | | `/proc/riptides/trust_anchors` | Trust anchor certificates (JSON) | | `/sys/module/riptides/certs/` | CA bundle, SPIFFE and intercept CAs | | `/sys/module/riptides/credentials/` | Credential files | `/proc/riptides/connections` lists **live** sockets only, so a connection that has already closed will not appear - sample it while traffic is flowing. Each entry carries an `alpn` field, which is what tells the TLS handling modes apart - `tls_version`, `mtls_version` and the SPIFFE ids look the same either way: | `alpn` | Meaning | | ---------------------- | ----------------------------------------------------- | | `riptides` | the module terminated TLS on this connection | | `riptides/passthrough` | it authenticated both ends and left the payload alone | | *(empty)* | no Riptides handshake ran on this connection | ```json {"dst": "127.0.0.1:6379", "tls_version": "TLS1.2", "mtls_version": "TLS1.3", "spiffe_id": "spiffe://example.console.riptides.io/cache/client", "peer_spiffe_id": "spiffe://example.console.riptides.io/cache/redis", "alpn": "riptides/passthrough"} ``` ### Asking the kernel about a connection [Section titled “Asking the kernel about a connection”](#asking-the-kernel-about-a-connection) The connections file above answers this for an operator looking at a node. An application can ask the same question about one of its *own* sockets with `getsockopt`, which is what you want in a test asserting that a connection really was protected, or when the application needs to behave differently depending on the answer: ```c #define SOL_RIPTIDES 7891 #define RIPTIDES_TLS_INFO 1 typedef struct { bool riptides_enabled; char mtls_type[16]; char spiffe_id[256]; char peer_spiffe_id[256]; char alpn[256]; } riptides_tls_info; ``` The `alpn` field is what identifies the mode: `riptides` means the module terminated TLS on this connection, and `riptides/passthrough` means it authenticated both ends and left the payload alone. ```python import ctypes, socket, ssl SOL_RIPTIDES, RIPTIDES_TLS_INFO = 7891, 1 class RiptidesTlsInfo(ctypes.Structure): _fields_ = [("riptides_enabled", ctypes.c_bool), ("mtls_type", ctypes.c_char * 16), ("spiffe_id", ctypes.c_char * 256), ("peer_spiffe_id", ctypes.c_char * 256), ("alpn", ctypes.c_char * 256)] raw = sock.getsockopt(SOL_RIPTIDES, RIPTIDES_TLS_INFO, ctypes.sizeof(RiptidesTlsInfo)) info = RiptidesTlsInfo.from_buffer_copy(raw) print(info.alpn, info.spiffe_id, info.peer_spiffe_id) ``` The call fails with `EOPNOTSUPP` when the socket is not a Riptides socket, or when its handshake has not completed yet - which is itself the answer to “is this connection protected?”. ### When evaluation or the handshake fails [Section titled “When evaluation or the handshake fails”](#when-evaluation-or-the-handshake-fails) If the daemon cannot answer within the module’s `command_timeout`, or an mTLS handshake fails - a missing trust anchor, for instance - the module’s behaviour depends on one parameter: ```bash cat /sys/module/riptides/parameters/eval_fail_closed # N by default ``` **By default the module fails open:** the connection proceeds *unprotected* rather than breaking. That keeps a control-plane problem from becoming an application outage, but it means losing protection is invisible from the application’s point of view - it still succeeds, and its logs still look healthy. The signals are `dmesg`, and `/proc/riptides/connections` showing connections with no `spiffe_id`. Set `eval_fail_closed=Y` to reset such connections instead, and monitor for it either way. You can also enable/disable the module at runtime without unloading it, and toggle debug logging via the kernel’s dynamic debug facility. ## Platform Support [Section titled “Platform Support”](#platform-support) The kernel module runs on Linux and supports: * **Architectures**: x86\_64, ARM64 * **Environments**: Bare metal, VMs, Kubernetes nodes, WSL2 on Windows hosts * **Installation**: Pre-built `.deb`/`.rpm` packages via the Riptides package repository * **IPv4 and IPv6** TCP sockets On Windows hosts the module loads inside the WSL2 VM, against Microsoft’s WSL2 kernel. That kernel is versioned separately from the distribution and is replaced by `wsl --update`, so the driver is matched to it per kernel version - see [WSL2 on Windows](/deployment/daemon-bare-metal/#wsl2-on-windows). # mTLS Without Service Definitions No `Service` resource or egress rule is required for internal mTLS between two Riptides-managed workloads, provided both peers share the same trust domain (CA) and the peers successfully identify each other as Riptides-managed. When those conditions are met, Riptides automatically establishes a full mutual TLS session with zero service-definition overhead. ## How It Works [Section titled “How It Works”](#how-it-works) Ordinarily, policy evaluation at connection time checks both whether the connecting workload has a valid identity (selector match) and whether a service rule authorizes the specific destination. Implicit mTLS splits this check into three stages: 1. **Identity resolution.** The daemon evaluates the connecting workload’s `WorkloadIdentity` selectors. If a match is found, the workload receives a valid SPIFFE identity, regardless of whether a service rule exists for this connection. 2. **Peer confirmation.** The peer must be Riptides-managed and share the same trust domain (CA). 3. **Certificate cross-check.** The SPIFFE SAN in the peer’s X.509 certificate must match the identity the peer announced during connection setup. A mismatch causes the connection to be rejected immediately. If either peer is not Riptides-managed or does not share the same trust domain (CA), the implicit mTLS conditions are not met and the connection falls back to plain TCP (observed but not intercepted). In those cases an explicit `Service` resource with egress rules is required. ## Minimal Configuration [Section titled “Minimal Configuration”](#minimal-configuration) No `Service` resource and no egress rules are required. Define a `WorkloadIdentity` for each workload: ```yaml apiVersion: core.riptides.io/v1alpha1 kind: WorkloadIdentity metadata: name: service-a namespace: riptides-system spec: workloadID: myapp/service-a selectors: - process:name: service-a scope: daemonGroup: id: daemongroup/production/workers ``` ```yaml apiVersion: core.riptides.io/v1alpha1 kind: WorkloadIdentity metadata: name: service-b namespace: riptides-system spec: workloadID: myapp/service-b selectors: - process:name: service-b scope: daemonGroup: id: daemongroup/production/workers ``` Once both `WorkloadIdentity` resources are applied, any connection between `service-a` and `service-b` is automatically secured with mTLS. No `Service` resource, no egress selectors, no `allowedSPIFFEIDs` lists needed. ## Security Characteristics [Section titled “Security Characteristics”](#security-characteristics) * **STRICT mTLS always.** Both sides must present a valid X.509 SVID issued by the shared trust domain. There is no plaintext fallback once implicit mTLS is active. * **Top-level `allowedSPIFFEIDs` are enforced.** If the `WorkloadIdentity` defines top-level `allowedSPIFFEIDs` (outbound on the client side, inbound on the server side), those restrictions apply even for implicit mTLS connections. Per-egress and per-ingress `allowedSPIFFEIDs` defined inside a `Service` do not apply - no service rule exists to select them. * **Identity confirmed via two channels.** The identity the peer announces during connection setup and the SPIFFE SAN in the TLS certificate must agree. A peer that announces one identity but presents a certificate for a different workload is rejected before any application data is exchanged. * **Only top-level workload identity is evaluated.** When no service rule exists, only the selector match that establishes the workload’s identity is considered. Egress label selectors, credential rules, and service-level `allowedSPIFFEIDs` defined inside a `Service` are not in scope - those apply only to explicitly configured connections. ## When to Use Explicit Service Definitions Instead [Section titled “When to Use Explicit Service Definitions Instead”](#when-to-use-explicit-service-definitions-instead) Implicit mTLS is the right default for trusted internal mesh traffic where any enrolled peer is acceptable. Use an explicit [`Service`](../services) resource with egress rules when you need: * **Per-destination caller restrictions** - `allowedSPIFFEIDs` on an egress or ingress rule limits connections to specific identities for a particular destination. Top-level `allowedSPIFFEIDs` on a `WorkloadIdentity` are enforced for implicit mTLS connections too, but they apply globally, not per-destination. * **Credential injection** - HTTP headers, AWS SigV4 signing, JWT credentials, or other credentials are attached per destination. * **External services** - Connections to endpoints that are not Riptides-managed (cloud APIs, third-party services, legacy systems). * **Per-destination TLS mode** - PERMISSIVE or SIMPLE mode on a specific destination, for example during a migration. ## Comparison [Section titled “Comparison”](#comparison) | | Implicit mTLS | Explicit Service + Egress | | ------------------------- | ---------------------------------------- | ----------------------------------------------------------- | | **Required config** | WorkloadIdentity only | WorkloadIdentity + Service + egress rules | | **TLS mode** | Always STRICT | Configurable (MUTUAL / PERMISSIVE / SIMPLE) | | **Caller restrictions** | Top-level `allowedSPIFFEIDs` only | Top-level + per-egress/ingress `allowedSPIFFEIDs` | | **Credential injection** | Not supported | Supported | | **External destinations** | Not applicable | Supported | | **Best for** | Internal mesh between enrolled workloads | Fine-grained access control, credentials, external services | # Node Attestation Node attestation establishes trust between a daemon and the control plane. When a daemon starts, it must prove its identity using a **Verifier** configured on the control plane. Only after successful node attestation does the daemon receive policies and certificates. This is distinct from [workload attestation](../workload-attestation), which happens afterward, at connection time, to identify individual processes on an already-attested node. ## How It Works [Section titled “How It Works”](#how-it-works) Each Verifier type implements a **proof mechanism** appropriate to the node’s environment: a signed cloud instance document, a JWT from a trusted issuer, proof of possession of a certificate, or a pre-shared token. The daemon (acting as a **Claimer**) produces a proof and sends it to the control plane; the control plane (acting as a **Verifier**) validates it and, on success, extracts a set of **metadata labels** from the proof. Some mechanisms are proof-of-possession based: after the initial proof is validated, the control plane issues a random nonce **challenge**, and the daemon must sign it with the private key associated with the proof to complete attestation. This prevents a stolen certificate or token from being replayed without also having the corresponding private key. Metadata labels are namespaced by verifier type, e.g. `awsiid:account:id` or `githubactions:repository:owner`. They serve two purposes: * **Scoping with `requiredMetadata`.** A Verifier can restrict which daemons it accepts by requiring specific metadata values, for example only EC2 instances in one AWS account, or only GitHub Actions runs from one repository. `requiredMetadata` is a list of key-value groups; a daemon matches if its metadata satisfies **all** keys within **any one** group (AND within a group, OR across groups): ```yaml requiredMetadata: - awsiid:account:id: "111111111111" awsiid:region: "us-east-1" - awsiid:account:id: "222222222222" awsiid:region: "eu-west-1" ``` This accepts daemons from account `111111111111` in `us-east-1`, **or** account `222222222222` in `eu-west-1`. * **Generating workload IDs with `workloadIDTemplate`.** A Go template string that renders the daemon’s `workloadID` from its attestation metadata, so IDs don’t have to be assigned manually. `requiredMetadata` is enforced (and rejected at admission time if missing) for the cloud verifiers (AWSIID, GCPIIT, AzureIMDS) and for GitHubActions, since without it any workload in the provider’s namespace (any AWS account, any GCP project, any GitHub org) could attest successfully. ## Verifier Types [Section titled “Verifier Types”](#verifier-types) | Verifier | Environment | Proof mechanism | Proof of possession challenge | | ----------------- | --------------- | ----------------------------------------------------------------------------------- | ----------------------------- | | **JoinToken** | Any | A pre-shared token created on the control plane. | Yes | | **AWSIID** | AWS | The signed EC2 Instance Identity Document. | No | | **GCPIIT** | GCP | A signed GCP Instance Identity Token (JWT). | No | | **AzureIMDS** | Azure | A signed Azure IMDS managed-identity token (JWT). | No | | **GitHubActions** | GitHub Actions | A signed OIDC token issued by GitHub’s Actions token endpoint. | No | | **K8sSAT** | Kubernetes | A projected Kubernetes Service Account Token, validated against the cluster’s JWKS. | No | | **JWT** | Any | A JWT from any trusted issuer. | No | | **X509CertPOP** | Any with PKI | An X.509 certificate issued by a trusted CA. | Yes | | **SSHCertPOP** | Any with SSH CA | An SSH certificate issued by a trusted CA. | Yes | Verifiers are defined as CRDs on the control plane. Multiple verifiers can coexist, allowing daemons in different environments to attest using the mechanism native to their platform. Full field-level configuration for each type is in the [Verifier reference](../../reference/verifier). ## Metadata by Provider [Section titled “Metadata by Provider”](#metadata-by-provider) The tables below list every metadata label each verifier can produce. Values marked “self-reported, unverified” are not part of the cryptographically signed proof and should not be relied on for security-sensitive scoping. ### JoinToken [Section titled “JoinToken”](#jointoken) | Key | Description | | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | | `jointoken:workload:id` | The `workloadID` configured on the matched JoinToken resource, if set. | | `jointoken:created-by:email` | The email in the JoinToken’s `riptides.io/created-by` annotation, if the token was created through the UI. | JoinToken carries no environmental metadata of its own; it only proves possession of the shared secret. Scoping is done by giving each token its own `workloadID`, not via `requiredMetadata`. ### AWSIID [Section titled “AWSIID”](#awsiid) Extracted from the EC2 Instance Identity Document: | Key | Description | | -------------------------- | ------------------------------------- | | `awsiid:architecture` | Instance CPU architecture. | | `awsiid:billing_products` | Billing product codes (multi-valued). | | `awsiid:startup_time` | The document’s `pendingTime`. | | `awsiid:private_ip` | Instance private IP address. | | `awsiid:region` | AWS region. | | `awsiid:availability:zone` | Availability zone. | | `awsiid:image:id` | AMI ID. | | `awsiid:account:id` | AWS account ID. | | `awsiid:instance:id` | EC2 instance ID. | | `awsiid:instance:type` | EC2 instance type. | | `awsiid:ramdisk:id` | Ramdisk ID, if present. | | `awsiid:kernel:id` | Kernel ID, if present. | ### GCPIIT [Section titled “GCPIIT”](#gcpiit) Extracted from the GCP Instance Identity Token claims: | Key | Description | | ---------------------------- | ---------------------------------------------------------------- | | `gcpiit:email` | Service account email. | | `gcpiit:instance:created_at` | Instance creation timestamp. | | `gcpiit:instance:name` | Compute Engine instance name. | | `gcpiit:instance:id` | Compute Engine instance ID. | | `gcpiit:project:id` | GCP project ID. | | `gcpiit:project:number` | GCP project number. | | `gcpiit:zone` | Compute Engine zone. | | `gcpiit:region` | Derived from the zone (e.g. `us-central1` from `us-central1-a`). | | `gcpiit:token:issuer` | Token `iss` claim. | | `gcpiit:token:audience` | Token `aud` claim. | | `gcpiit:token:subject` | Token `sub` claim. | ### AzureIMDS [Section titled “AzureIMDS”](#azureimds) Extracted from the Azure IMDS managed-identity token claims and the `xms_mirid` resource ID: | Key | Description | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `azureimds:tenant:id` | Azure AD tenant ID (`tid` claim). | | `azureimds:identity:object_id` | Managed identity object ID (`oid` claim). | | `azureimds:identity:client_id` | Managed identity client ID (`appid` on v1 tokens, `azp` on v2). | | `azureimds:token:subject` | Token `sub` claim. | | `azureimds:token:issuer` | Token `iss` claim. | | `azureimds:subscription:id` | Azure subscription ID, parsed from `xms_mirid`. | | `azureimds:resource_group` | Resource group name, parsed from `xms_mirid`. | | `azureimds:instance:name` | VM name, parsed from `xms_mirid` (system-assigned identity on a VM). | | `azureimds:instance:scale_set` | VMSS name, parsed from `xms_mirid` (system-assigned identity on a scale set). | | `azureimds:identity:name` | User-assigned identity name, parsed from `xms_mirid` (e.g. AKS nodes, which share the kubelet identity). | | `azureimds:instance:vm_id` | **Self-reported, unverified.** The node’s VM ID from the unsigned IMDS instance-metadata endpoint. Used as the only per-node discriminator on AKS, where every node shares the same managed identity and JWT claims. Never overrides a JWT-derived label. | ### GitHubActions [Section titled “GitHubActions”](#githubactions) Extracted from the GitHub Actions OIDC token claims: | Key | Description | | ------------------------------------ | ------------------------------------------------- | | `githubactions:repository:full_name` | `org/repo`. | | `githubactions:repository:owner` | GitHub org or user. | | `githubactions:workflow` | Workflow name. | | `githubactions:run:id` | Workflow run ID. | | `githubactions:actor` | User that triggered the run. | | `githubactions:ref` | Git ref (e.g. `refs/heads/main`). | | `githubactions:environment` | Deployment environment, if the job targets one. | | `githubactions:event:name` | Triggering event (e.g. `push`, `pull_request`). | | `githubactions:runner:environment` | `github-hosted` or `self-hosted`. | | `githubactions:sha` | Commit SHA. | | `githubactions:job:workflow_ref` | Reusable workflow ref, if the job comes from one. | | `githubactions:token:issuer` | Token `iss` claim. | | `githubactions:token:subject` | Token `sub` claim. | | `githubactions:token:id` | Token `jti` claim. | ### K8sSAT [Section titled “K8sSAT”](#k8ssat) Extracted from the Kubernetes Service Account Token claims: | Key | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `k8ssat:namespace` | Pod namespace. | | `k8ssat:node:name` | Node name. | | `k8ssat:node:uid` | Node UID. | | `k8ssat:pod:name` | Pod name. | | `k8ssat:pod:uid` | Pod UID. | | `k8ssat:sa:name` | Service account name. | | `k8ssat:sa:uid` | Service account UID. | | `k8ssat:token:audience` | Token `aud` claim. | | `k8ssat:token:issuer` | Token `iss` claim. | | `k8ssat:token:id` | Token `jti` claim. | | `k8ssat:token:subject` | Token `sub` claim. | | `k8ssat:cluster:id` | The `clusterID` configured on the matching Verifier. Added by the control plane rather than derived from the token, so every daemon attesting through a given K8sSAT verifier carries the same cluster identifier. | #### Getting the cluster’s JWKS [Section titled “Getting the cluster’s JWKS”](#getting-the-clusters-jwks) The K8sSAT verifier validates the token’s signature against the issuing cluster’s JWKS, configured via `jwksSource` as either an inline JWKS document or a `remote.url` the control plane fetches it from. Every Kubernetes API server with a service account issuer configured, which is the default on managed offerings, publishes an OIDC discovery document at `/.well-known/openid-configuration` containing a `jwks_uri` field; that URI is what you point `jwksSource.remote.url` at (or fetch once and paste into `jwksSource.inline`, for a cluster the control plane can’t reach directly). * **EKS**: The cluster has a public OIDC issuer URL (`aws eks describe-cluster --name --query "cluster.identity.oidc.issuer"`); its JWKS is publicly reachable without cluster credentials. See the [EKS OIDC provider docs](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html). * **GKE**: Run `kubectl get --raw /.well-known/openid-configuration` against the cluster and read the `jwks_uri` field from the response. See the [GKE workload identity docs](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). * **AKS**: Requires the OIDC issuer feature enabled on the cluster (`az aks update --enable-oidc-issuer`); the issuer URL is then available via `az aks show --query "oidcIssuerProfile.issuerUrl"`. See the [AKS OIDC issuer docs](https://learn.microsoft.com/en-us/azure/aks/use-oidc-issuer). For a self-managed or on-premises cluster, the same `/.well-known/openid-configuration` lookup works as long as `--service-account-issuer` is configured on the API server. ### JWT [Section titled “JWT”](#jwt) The generic JWT verifier maps token claims to metadata using its `metadataMap` field. Without any additional mapping, it produces: | Key | Description | | -------------- | ------------------ | | `jwt:audience` | Token `aud` claim. | | `jwt:issuer` | Token `iss` claim. | | `jwt:id` | Token `jti` claim. | | `jwt:subject` | Token `sub` claim. | Add entries to `metadataMap` to expose custom claims under additional `jwt: