Agent plugin: WorkloadAttestor "k8s"

August 6, 2026 ยท View on GitHub

The k8s plugin generates Kubernetes-based selectors for workloads calling the agent. It does so by retrieving the workload's pod ID from its cgroup membership, then querying the kubelet for information about the pod.

The plugin can talk to the kubelet via the insecure read-only port or the secure port. Both X509 client authentication and bearer token (e.g. service account token) authentication to the secure port is supported.

Verifying the certificate presented by the kubelet over the secure port is optional. The default is to verify, based on the certificate file passed via kubelet_ca_path. skip_kubelet_verification can be set to disable verification.

The agent will contact the kubelet using the node name obtained via the node_name_env or node_name configurables. If a node name is not obtained, the kubelet is contacted over 127.0.0.1 (requires host networking to be enabled). In the latter case, the hostname is used to perform certificate server name validation against the kubelet certificate. Set disable_kubelet_client = true only for broker-only deployments that do not need PID-based workload attestation or agent-node-scoped pod references.

Note kubelet authentication via bearer token requires that the kubelet be started with the --authentication-token-webhook flag. See Kubelet authentication/authorization for details.

Note The kubelet uses the TokenReview API to validate bearer tokens. This requires reachability to the Kubernetes API server. Therefore, API server downtime can interrupt workload attestation. The --authentication-token-webhook-cache-ttl kubelet flag controls how long the kubelet caches TokenReview responses and may help to mitigate this issue. A large cache ttl value is not recommended however, as that can impact permission revocation.

Note Anonymous authentication with the kubelet requires that the kubelet be started with the --anonymous-auth flag. It is discouraged to use anonymous auth mode in production as it requires authorizing anonymous users to the nodes/proxy resource that maps to some privileged operations, such as executing commands in containers and reading pod logs.

Note To run on Windows containers, Kubernetes v1.24+ and containerd v1.6+ are required, since hostprocess container is required on the agent container.

ConfigurationDescription
disable_container_selectorsIf true, container selectors are not produced. This can be used to produce pod selectors when the workload pod is known but the workload container is not ready at the time of attestation.
disable_kubelet_clientIf true, disables kubelet client setup and kubelet pod-list calls. PID-based workload attestation and pod_reference_scope = "agent_node" pod references are unavailable. Mutually exclusive with kubelet client settings.
enable_namespace_labelsIf true, namespace labels are fetched from the Kubernetes API server and produced as selectors. Requires RBAC get, list and watch on namespaces. Disabled by default.
kubelet_read_only_portThe kubelet read-only port. This is mutually exclusive with kubelet_secure_port.
kubelet_secure_portThe kubelet secure port. It defaults to 10250 unless kubelet_read_only_port is set.
kubelet_ca_pathThe path on disk to a file containing CA certificates used to verify the kubelet certificate. Required unless skip_kubelet_verification is set. Defaults to the cluster CA bundle /var/run/secrets/kubernetes.io/serviceaccount/ca.crt.
skip_kubelet_verificationIf true, kubelet certificate verification is skipped.
token_pathThe path on disk to the bearer token used for kubelet authentication. Defaults to the service account token /var/run/secrets/kubernetes.io/serviceaccount/token.
certificate_pathThe path on disk to client certificate used for kubelet authentication.
private_key_pathThe path on disk to client key used for kubelet authentication.
use_anonymous_authenticationIf true, use anonymous authentication for kubelet communication.
node_name_envThe environment variable used to obtain the node name for kubelet requests. Defaults to MY_NODE_NAME.
node_nameThe node name used for kubelet requests. Overrides the value obtained from the env var specified by node_name_env.
max_poll_attemptsThe maximum number of kubelet polling attempts while locating the workload container/pod. Defaults to 60.
poll_retry_intervalThe interval between kubelet polling attempts while locating the workload container/pod. Defaults to 500ms.
reload_intervalHow often kubelet TLS and token configuration is reloaded from disk. Defaults to 1m.
experimentalThe experimental options that are subject to change or removal (see below).
sigstoreSigstore options. Options described below. See Sigstore options. When set, enables verification of container image signatures and attestations.
use_new_container_locatorIf true, enables the new container locator algorithm that has support for cgroups v2. Defaults to true.
verbose_container_locator_logsIf true, enables verbose logging of mountinfo and cgroup information used to locate containers. Defaults to false.

These are the current experimental configurations.

experimentalDescriptionDefault
api_server.cache.enabledIf true, enables a controller-runtime Kubernetes API server cache for object-reference lookups.false
brokerBroker API options for AttestReference. Required when this plugin handles Broker API references. See Broker API.

Sigstore feature

This feature extends the k8s workload attestor with the ability to validate container image signatures and attestations using the Sigstore ecosystem. It is optional and only enabled when the sigstore block is configured.

Sigstore options

OptionDescription
allowed_identitiesMaps OIDC Provider URIs to lists of allowed subjects. Supports regular expressions patterns. Defaults to empty. If unspecified, signatures from any issuer are accepted. (eg. "https://accounts.google.com" = ["subject1@example.com","subject2@example.com"]).
skipped_imagesLists image IDs to exclude from Sigstore signature verification. For these images, no Sigstore selectors will be generated. Defaults to an empty list.
rekor_urlSpecifies the Rekor URL for transparency log verification. Default is the public Rekor instance https://rekor.sigstore.dev.
ignore_tlogIf set to true, bypasses the transparency log verification and the selectors based on the Rekor bundle are not generated.
ignore_attestationsIf set to true, bypasses the image attestations verification and the selector image-attestations:verified is not generated.
ignore_sctIf set to true, bypasses the Signed Certificate Timestamp (SCT) verification.
registry_credentialsMaps each registry URL to its corresponding authentication credentials. Example: {"docker.io": {"username": "user", "password": "pass"}}.

Custom CA Roots

Custom CA roots signed through TUF can be provided using the cosign initialize command. This method securely pins the CA roots, ensuring that only trusted certificates are used during validation. Additionally, trusted roots for certificate validation can be specified via the SIGSTORE_ROOT_FILE environment variable. For more details on Cosign configurations, refer to the documentation.

K8s selectors

SelectorValue
k8s:nsThe workload's namespace
k8s:ns-labelA label on the workload's namespace (available when enable_namespace_labels is true)
k8s:saThe workload's service account
k8s:container-imageThe Image OR ImageID of the container in the workload's pod which is requesting an SVID, as reported by K8S. Selector value may be an image tag, such as: docker.io/envoyproxy/envoy-alpine:v1.16.0, or a resolved SHA256 image digest, such as docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb. See image selector limitations for important caveats when using tag-based selectors.
k8s:container-nameThe name of the workload's container
k8s:node-nameThe name of the workload's node
k8s:pod-labelA label given to the workload's pod
k8s:pod-ownerThe name of the workload's pod owner
k8s:pod-owner-uidThe UID of the workload's pod owner
k8s:pod-uidThe UID of the workload's pod
k8s:pod-nameThe name of the workload's pod
k8s:pod-imageAn Image OR ImageID of any container in the workload's pod, as reported by K8S. Selector value may be an image tag, such as: docker.io/envoyproxy/envoy-alpine:v1.16.0, or a resolved SHA256 image digest, such as docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb. See image selector limitations for important caveats when using tag-based selectors.
k8s:pod-image-countThe number of container images in workload's pod
k8s:pod-init-imageAn Image OR ImageID of any init container in the workload's pod, as reported by K8S. Selector value may be an image tag, such as: docker.io/envoyproxy/envoy-alpine:v1.16.0, or a resolved SHA256 image digest, such as docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb. See image selector limitations for important caveats when using tag-based selectors.
k8s:pod-init-image-countThe number of init container images in workload's pod

Sigstore enabled selectors (available when configured to use sigstore)

SelectorValue
k8s:image-signature:verifiedWhen the image signature was verified and is valid.
k8s:image-attestations:verifiedWhen the image attestations were verified and are valid.
k8s:image-signature-valueThe base64 encoded value of the signature (eg. k8s:image-signature-value:MEUCIQCyem8Gcr0sPFMP7fTXazCN57NcN5+MjxJw9Oo0x2eM+AIg...).
k8s:image-signature-subjectThe OIDC principal that signed the image (e.g., k8s:image-signature-subject:spirex@example.com)
k8s:image-signature-issuerThe OIDC issuer of the signature (e.g., k8s:image-signature-issuer:https://accounts.google.com)
k8s:image-signature-log-idA unique LogID for the Rekor transparency log entry (eg. k8s:image-signature-log-id:c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b95918123)
k8s:image-signature-log-indexThe log index for the Rekor transparency log entry (eg. k8s:image-signature-log-index:105695637)
k8s:image-signature-integrated-timeThe time (in Unix timestamp format) when the image signature was integrated into the signature transparency log (eg. k8s:image-signature-integrated-time:1719237832)
k8s:image-signature-signed-entry-timestampThe base64 encoded signed entry (signature over the logID, logIndex, body and integratedTime) (eg. k8s:image-signature-signed-entry-timestamp:MEQCIDP77vB0/MEbR1QK...).

If ignore_tlog is set to true, the selectors based on the Rekor bundle (-log-id, -log-index, -integrated-time, and -signed-entry-timestamp) are not generated.

Note container-image will ONLY match against the specific container in the pod that is contacting SPIRE on behalf of the pod, whereas pod-image and pod-init-image will match against ANY container or init container in the Pod, respectively.

Broker API

When SPIRE Agent's SPIFFE Broker API is enabled, the k8s workload attestor handles Broker API AttestReference requests for WorkloadPIDReference and KubernetesObjectReference. AttestReference requires an experimental.broker block in the plugin configuration. Each experimental.broker.brokers entry identifies one broker SPIFFE ID that may use this plugin. Broker IDs must be valid, unique, and non-empty. The required block-level access_policy setting controls whether the plugin creates Kubernetes SubjectAccessReview requests for resolved objects. Use access_policy = "enforced" to authorize every resolved object with Kubernetes before selectors are returned. Use access_policy = "permissive" to skip that authorization check. Each broker may set pod_reference_scope to agent_node (default) or cluster; this only affects pod KubernetesObjectReference resolution. Broker-only agents that set disable_kubelet_client = true must configure every broker with pod_reference_scope = "cluster".

Example:

WorkloadAttestor "k8s" {
  plugin_data {
    experimental {
      broker {
        access_policy = "enforced"
        brokers = [
          {
            id = "spiffe://example.org/broker"
            pod_reference_scope = "cluster"
          }
        ]
      }
    }
  }
}

WorkloadPIDReference follows the PID-based k8s attestation path to resolve the workload pod and selectors. When that Broker API reference resolves to a pod and experimental.broker.access_policy = "enforced", the plugin creates a SubjectAccessReview asking whether the broker SPIFFE ID may use SPIRE's custom Kubernetes authorization verb impersonate-via-spire on the resolved pod. The review uses the broker SPIFFE ID as the SAR username and does not set groups. PID-based attestation via the workload attestor's Attest RPC does not use the broker configuration or run this review.

For KubernetesObjectReference, the reference identifies the target object by its resource (<plural>.<group>, with core as the group string for core resources) and either its namespaced name (namespace + name), its uid, or both. Pod references use the local kubelet pod list as a fast path when a kubelet client is available. With the default pod_reference_scope = "agent_node", the kubelet client is required and pod references are limited to information returned by the local kubelet. With pod_reference_scope = "cluster", a disabled or unavailable kubelet client does not prevent resolution: the plugin falls back to the Kubernetes API server and can resolve pods on any node. Non-pod object references are resolved through the Kubernetes API server. When experimental.broker.access_policy = "enforced", the plugin then creates the same SubjectAccessReview for the referenced object. The review uses the broker SPIFFE ID as the SAR username, no groups, the reference's resource group and plural, and the resolved namespace and name. If the authorizer denies the review, attestation fails with PermissionDenied. Kubernetes API server lookups require the agent ServiceAccount to have permission for the referenced resource.

Pods (pods/core). A KubernetesObjectReference to a pod emits the same pod-shaped selectors documented in the table above (k8s:ns, k8s:sa, k8s:pod-name, k8s:container-name, k8s:pod-uid, k8s:pod-label, k8s:pod-image, k8s:pod-owner, ...). By default, broker pod references are limited to pods returned by the local kubelet (agent_node). Set pod_reference_scope = "cluster" for a broker that must reference pods on other nodes through the Kubernetes API server, or when the agent is configured with disable_kubelet_client = true. A registration entry written for the PID-based flow continues to match either reference type.

Other resources (any <plural>.<group> for which the agent has permission). The agent fetches the object's metadata via the Kubernetes API server (using a PartialObjectMetadata request) and emits a uniform vocabulary that is independent of the resource's kind:

SelectorValue
k8s:uidThe object's UID.
k8s:resource<plural>.<group> for the resource (e.g. deployments.apps, pods.core).
k8s:pluralThe resource plural (e.g. deployments).
k8s:groupThe API group (e.g. apps); core for core resources.
k8s:versionThe discovered version (e.g. v1, v1beta1).
k8s:apiVersionThe Kubernetes wire form: v1 for core or <group>/<version> otherwise.
k8s:kindThe object kind (e.g. Deployment).
k8s:nameThe object name.
k8s:namespaceThe object namespace; omitted for cluster-scoped objects.
k8s:key<namespace>/<name> for namespaced objects, or just <name> for cluster-scoped ones.
k8s:labelA label on the object, formatted <key>:<value> (one selector per label entry).
k8s:owner-key<group>/<Kind>/<name> for each entry in metadata.ownerReferences.
k8s:owner-uid<group>/<Kind>/<uid> for each entry in metadata.ownerReferences.
k8s:controller-keySame as k8s:owner-key, but only emitted for owner references with Controller: true.
k8s:controller-uidSame as k8s:owner-uid, but only emitted for owner references with Controller: true.

Annotations are intentionally not exposed as selectors. Kubernetes labels are validated and indexed by the API server and are the appropriate identity anchor; annotations are an unconstrained metadata grab bag (often multi-line JSON) that does not fit equality-matched selectors.

The agent's ServiceAccount needs get (and list when references identify objects by uid alone) permission on every resource it is expected to resolve via this path. When experimental.broker.access_policy = "enforced", it also needs create on subjectaccessreviews.authorization.k8s.io to perform broker authorization checks. For example:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: spire-agent-k8s-attestor
rules:
  # Object-reference resolution. Add every resource brokers may reference.
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
  - apiGroups: ["kustomize.toolkit.fluxcd.io"]
    resources: ["kustomizations"]
    verbs: ["get", "list"]

  # Required only when experimental.broker.access_policy = "enforced", so the k8s workload
  # attestor can create SubjectAccessReview objects while handling Broker API
  # reference requests.
  - apiGroups: ["authorization.k8s.io"]
    resources: ["subjectaccessreviews"]
    verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: spire-agent-k8s-attestor
subjects:
  - kind: ServiceAccount
    name: spire-agent
    namespace: spire
roleRef:
  kind: ClusterRole
  name: spire-agent-k8s-attestor
  apiGroup: rbac.authorization.k8s.io

When experimental.broker.access_policy = "enforced", the Kubernetes authorizer must also allow the broker SPIFFE ID to use SPIRE's custom impersonate-via-spire verb on the resources brokers may reference. The SubjectAccessReview checks the broker SPIFFE ID as a Kubernetes username and does not set groups. The broker pod's Kubernetes ServiceAccount is not used as the reviewed subject.

SPIRE intentionally does not use Kubernetes' built-in impersonate verb for this check. The built-in verb has broader meaning in Kubernetes RBAC: it can authorize native impersonation of users, groups, ServiceAccounts, and other impersonation targets. Granting that built-in verb to broker-related Kubernetes identities would create powerful RBAC subjects with permissions outside SPIRE's broker authorization decision. The impersonate-via-spire verb is a SPIRE-specific authorization gate checked only by this SubjectAccessReview; granting it lets Kubernetes answer SPIRE's question without granting Kubernetes-native impersonation power.

For example, with broker ID spiffe://example.org/broker:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: spire-broker-impersonation
rules:
  # This is the broker authorization decision enforced by SubjectAccessReview.
  # Use SPIRE's custom verb, not Kubernetes' built-in `impersonate` verb.
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["impersonate-via-spire"]
  - apiGroups: ["kustomize.toolkit.fluxcd.io"]
    resources: ["kustomizations"]
    verbs: ["impersonate-via-spire"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: spire-broker-impersonation
subjects:
  - kind: User
    name: spiffe://example.org/broker
roleRef:
  kind: ClusterRole
  name: spire-broker-impersonation
  apiGroup: rbac.authorization.k8s.io

A Role and RoleBinding can be used instead when brokers should only be allowed to authorize SPIRE broker references for namespaced resources in a specific namespace.

Image selector limitations

The container-image, pod-image, and pod-init-image selectors are derived from two fields in the Kubernetes ContainerStatus: Image (typically the tag-based name, e.g. myimage:v1.2.3) and ImageID (typically the digest-based identifier, e.g. myimage@sha256:abc...). Both values are emitted as selectors for each container to support matching by either form.

Tag-based image names can be inconsistent. The Image field is populated by the container runtime via the CRI API, which does not standardize which image name to return when a single image digest is associated with multiple tags. For example, if myimage:latest and myimage:v1.2.3 both refer to the same SHA256 digest, the container runtime may report either tag in the Image field, and the choice can vary across nodes or over time. This has been observed in multiple CRI implementations (e.g. cri-dockerd).

As a result, registration entries that rely on tag-based image selectors may produce inconsistent attestation results. A workload deployed with myimage:v1.2.3 could be attested with a container-image:myimage:latest selector (or vice versa) if both tags resolve to the same digest on that node.

Recommendation: For reliable workload registration, prefer using digest-based image identifiers (the ImageID form) in selectors rather than tag-based names. For example, use:

k8s:container-image:docker.io/envoyproxy/envoy-alpine@sha256:bf862e5f5eca0a73e7e538224578c5cf867ce2be91b5eaed22afc153c00363eb

instead of:

k8s:container-image:docker.io/envoyproxy/envoy-alpine:v1.16.0

Tag-based selectors remain useful for human readability and for dynamic workload registration scenarios where the digest is not yet known (e.g. before an image pull is initiated). However, operators should be aware of the trade-off between readability and uniqueness, and avoid relying on tag-based image selectors in environments where multiple tags may reference the same image digest. See #4287 for more details.

Examples

To use the kubelet read-only port:

WorkloadAttestor "k8s" {
  plugin_data {
    kubelet_read_only_port = 10255
  }
}

To use the secure kubelet port, verify via /var/run/secrets/kubernetes.io/serviceaccount/ca.crt, and authenticate via the default service account token:

WorkloadAttestor "k8s" {
  plugin_data {
  }
}

To use the secure kubelet port, skip verification, and authenticate via the default service account token:

WorkloadAttestor "k8s" {
  plugin_data {
    skip_kubelet_verification = true
  }
}

To use the secure kubelet port, skip verification, and authenticate via some other token:

WorkloadAttestor "k8s" {
  plugin_data {
    skip_kubelet_verification = true
    token_path = "/path/to/token"
  }
}

To use the secure kubelet port, verify the kubelet certificate, and authenticate via an X509 client certificate:

WorkloadAttestor "k8s" {
  plugin_data {
    kubelet_ca_path = "/path/to/kubelet-ca.pem"
    certificate_path = "/path/to/cert.pem"
    private_key_path = "/path/to/key.pem"
  }
}

To enable the Kubernetes API server cache used by object-reference lookups:

WorkloadAttestor "k8s" {
  plugin_data {
    experimental {
      api_server {
        cache {
          enabled = true
        }
      }
    }
  }
}

Platform support

This plugin is supported on Unix systems and Windows hostprocess containers.

Known issues

  • This plugin may fail to correctly attest workloads in pods that use lifecycle hooks to alter pod start behavior. This includes Istio workloads when the holdApplicationUntilProxyStarts configurable is set to true. Please see #3092 for more information. The disable_container_selectors configurable can be used to successfully attest workloads in this situation, albeit with reduced selector granularity (i.e. pod selectors only).

  • Tag-based image selectors (container-image, pod-image, pod-init-image) can produce inconsistent attestation results when multiple image tags reference the same digest. The container runtime may non-deterministically report any of the associated tags, which can cause workloads to fail attestation on some nodes. Use digest-based image identifiers for reliable matching. See the image selector limitations section and #4287 for more information.