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.
How It Works
Section titled “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.requiredMetadatais 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
111111111111inus-east-1, or account222222222222ineu-west-1. -
Generating workload IDs with
workloadIDTemplate. A Go template string that renders the daemon’sworkloadIDfrom 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 | 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.
Metadata by Provider
Section titled “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”| 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”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”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”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”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”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”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-configurationagainst the cluster and read thejwks_urifield 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 viaaz 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:
| 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:<label> keys.
X509CertPOP
Section titled “X509CertPOP”Extracted from the presented X.509 certificate:
| Key | Description |
|---|---|
x509certpop:subject:commonName | Certificate subject CN. |
x509certpop:subject:organization | Certificate subject O (multi-valued). |
x509certpop:subject:organizationUnit | Certificate subject OU (multi-valued). |
x509certpop:subject:country | Certificate subject C (multi-valued). |
x509certpop:serial | Certificate serial number (hex). |
x509certpop:issuer | Certificate issuer DN. |
x509certpop:dns | Certificate SAN DNS names (multi-valued). |
x509certpop:email | Certificate SAN email addresses (multi-valued). |
x509certpop:uri | Certificate SAN URIs (multi-valued). |
x509certpop:ip | Certificate SAN IP addresses (multi-valued). |
SSHCertPOP
Section titled “SSHCertPOP”Extracted from the presented SSH certificate:
| Key | Description |
|---|---|
sshcertpop:hash | SHA-256 hash of the certificate. |
sshcertpop:principal | Valid principals on the certificate (multi-valued). |
Example: JoinToken Verifier
Section titled “Example: JoinToken Verifier”apiVersion: auth.riptides.io/v1alpha1kind: Verifiermetadata: name: dev-jointoken namespace: riptides-systemspec: joinToken: {}---apiVersion: auth.riptides.io/v1alpha1kind: JoinTokenmetadata: name: daemon-token-001 namespace: riptides-systemspec: token: "<token-value>"Example: AWS Instance Identity Document
Section titled “Example: AWS Instance Identity Document”apiVersion: auth.riptides.io/v1alpha1kind: Verifiermetadata: name: aws-verifier namespace: riptides-systemspec: 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.