Skip to content

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, which happens afterward, at connection time, to identify individual processes on an already-attested node.

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):

    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.

VerifierEnvironmentProof mechanismProof of possession challenge
JoinTokenAnyA pre-shared token created on the control plane.Yes
AWSIIDAWSThe signed EC2 Instance Identity Document.No
GCPIITGCPA signed GCP Instance Identity Token (JWT).No
AzureIMDSAzureA signed Azure IMDS managed-identity token (JWT).No
GitHubActionsGitHub ActionsA signed OIDC token issued by GitHub’s Actions token endpoint.No
K8sSATKubernetesA projected Kubernetes Service Account Token, validated against the cluster’s JWKS.No
JWTAnyA JWT from any trusted issuer.No
X509CertPOPAny with PKIAn X.509 certificate issued by a trusted CA.Yes
SSHCertPOPAny with SSH CAAn 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.

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.

KeyDescription
jointoken:workload:idThe workloadID configured on the matched JoinToken resource, if set.
jointoken:created-by:emailThe 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.

Extracted from the EC2 Instance Identity Document:

KeyDescription
awsiid:architectureInstance CPU architecture.
awsiid:billing_productsBilling product codes (multi-valued).
awsiid:startup_timeThe document’s pendingTime.
awsiid:private_ipInstance private IP address.
awsiid:regionAWS region.
awsiid:availability:zoneAvailability zone.
awsiid:image:idAMI ID.
awsiid:account:idAWS account ID.
awsiid:instance:idEC2 instance ID.
awsiid:instance:typeEC2 instance type.
awsiid:ramdisk:idRamdisk ID, if present.
awsiid:kernel:idKernel ID, if present.

Extracted from the GCP Instance Identity Token claims:

KeyDescription
gcpiit:emailService account email.
gcpiit:instance:created_atInstance creation timestamp.
gcpiit:instance:nameCompute Engine instance name.
gcpiit:instance:idCompute Engine instance ID.
gcpiit:project:idGCP project ID.
gcpiit:project:numberGCP project number.
gcpiit:zoneCompute Engine zone.
gcpiit:regionDerived from the zone (e.g. us-central1 from us-central1-a).
gcpiit:token:issuerToken iss claim.
gcpiit:token:audienceToken aud claim.
gcpiit:token:subjectToken sub claim.

Extracted from the Azure IMDS managed-identity token claims and the xms_mirid resource ID:

KeyDescription
azureimds:tenant:idAzure AD tenant ID (tid claim).
azureimds:identity:object_idManaged identity object ID (oid claim).
azureimds:identity:client_idManaged identity client ID (appid on v1 tokens, azp on v2).
azureimds:token:subjectToken sub claim.
azureimds:token:issuerToken iss claim.
azureimds:subscription:idAzure subscription ID, parsed from xms_mirid.
azureimds:resource_groupResource group name, parsed from xms_mirid.
azureimds:instance:nameVM name, parsed from xms_mirid (system-assigned identity on a VM).
azureimds:instance:scale_setVMSS name, parsed from xms_mirid (system-assigned identity on a scale set).
azureimds:identity:nameUser-assigned identity name, parsed from xms_mirid (e.g. AKS nodes, which share the kubelet identity).
azureimds:instance:vm_idSelf-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.

Extracted from the GitHub Actions OIDC token claims:

KeyDescription
githubactions:repository:full_nameorg/repo.
githubactions:repository:ownerGitHub org or user.
githubactions:workflowWorkflow name.
githubactions:run:idWorkflow run ID.
githubactions:actorUser that triggered the run.
githubactions:refGit ref (e.g. refs/heads/main).
githubactions:environmentDeployment environment, if the job targets one.
githubactions:event:nameTriggering event (e.g. push, pull_request).
githubactions:runner:environmentgithub-hosted or self-hosted.
githubactions:shaCommit SHA.
githubactions:job:workflow_refReusable workflow ref, if the job comes from one.
githubactions:token:issuerToken iss claim.
githubactions:token:subjectToken sub claim.
githubactions:token:idToken jti claim.

Extracted from the Kubernetes Service Account Token claims:

KeyDescription
k8ssat:namespacePod namespace.
k8ssat:node:nameNode name.
k8ssat:node:uidNode UID.
k8ssat:pod:namePod name.
k8ssat:pod:uidPod UID.
k8ssat:sa:nameService account name.
k8ssat:sa:uidService account UID.
k8ssat:token:audienceToken aud claim.
k8ssat:token:issuerToken iss claim.
k8ssat:token:idToken jti claim.
k8ssat:token:subjectToken sub claim.
k8ssat:cluster:idThe 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.

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 <cluster> --query "cluster.identity.oidc.issuer"); its JWKS is publicly reachable without cluster credentials. See the EKS OIDC provider docs.
  • 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.
  • 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.

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.

The generic JWT verifier maps token claims to metadata using its metadataMap field. Without any additional mapping, it produces:

KeyDescription
jwt:audienceToken aud claim.
jwt:issuerToken iss claim.
jwt:idToken jti claim.
jwt:subjectToken sub claim.

Add entries to metadataMap to expose custom claims under additional jwt:<label> keys.

Extracted from the presented X.509 certificate:

KeyDescription
x509certpop:subject:commonNameCertificate subject CN.
x509certpop:subject:organizationCertificate subject O (multi-valued).
x509certpop:subject:organizationUnitCertificate subject OU (multi-valued).
x509certpop:subject:countryCertificate subject C (multi-valued).
x509certpop:serialCertificate serial number (hex).
x509certpop:issuerCertificate issuer DN.
x509certpop:dnsCertificate SAN DNS names (multi-valued).
x509certpop:emailCertificate SAN email addresses (multi-valued).
x509certpop:uriCertificate SAN URIs (multi-valued).
x509certpop:ipCertificate SAN IP addresses (multi-valued).

Extracted from the presented SSH certificate:

KeyDescription
sshcertpop:hashSHA-256 hash of the certificate.
sshcertpop:principalValid principals on the certificate (multi-valued).
apiVersion: auth.riptides.io/v1alpha1
kind: Verifier
metadata:
name: dev-jointoken
namespace: riptides-system
spec:
joinToken: {}
---
apiVersion: auth.riptides.io/v1alpha1
kind: JoinToken
metadata:
name: daemon-token-001
namespace: riptides-system
spec:
token: "<token-value>"
apiVersion: auth.riptides.io/v1alpha1
kind: Verifier
metadata:
name: aws-verifier
namespace: riptides-system
spec:
AWSIID: {}
requiredMetadata:
- awsiid:account:id: "<aws-account-id>"
awsiid:region: "us-east-1"

This verifier accepts daemons running on EC2 instances in the specified account and region. The daemon automatically retrieves the instance identity document from the EC2 metadata service and presents it during attestation.