- A 64-bit Linux environment (AMD, Intel, or Arm).
- Basic familiarity with
curl
commands is helpful.
Ahoy, digital navigators! 🏴☠️ Welcome aboard Coastal Containers Ltd, a renowned trans-Atlantic freight shipping company. As we set sail on this voyage, we find ourselves amidst turbulent waters. The legacy shipping systems, once the pride of our fleet, now face threats from modern-day digital pirates. But fear not! For we have embarked on a quest to modernize and fortify our defenses using the formidable technologies of SPIFFE and SPIRE, aiming to implement a zero-trust security model.
In this adventurous lab, our main objective is to chart the course through the intricate waves of OpenID Connect (OIDC) Discovery and its integration with SPIRE. By the end of this journey, you'll should grasp:
- 🚢 Anchoring SPIRE with Helm: Setting up our trusty SPIRE using the Helm package manager.
- 🧭 Navigating SPIRE's Configuration: Gaining insights into the SPIRE Server and Agent configurations.
- 🌊 Understanding the Digital Ocean: Grasping the concepts of JWTs, Ingress, Certificates, and DNS in the vast sea of SPIRE.
- ⚓ Deploying and Verifying Workloads: Ensuring our workload 'cargo' is safely secured within the SPIRE environment.
Our mission is clear: to integrate OIDC Discovery with SPIRE, ensuring that the treasured cargo of Coastal Containers Ltd. is safeguarded against swashbucklin' pirates up to no good! So, brace yourselves, for we are about to embark on a thrilling journey where technology meets adventure, and where we, the crew of Coastal Containers Ltd., strive to secure our legacy for the future generations to come! 🌊🔐🏴☠️
Run the following command in the lab-08-oidc-discovery directory to set up the necessary infrastructure, including cert-manager, contour and a self-signed CA:
make cluster-up
Before you deploy SPIRE via the Helm chart, you must first add the SPIFFE helm repo by running:
make spire-add-helm-repo
Once added, install SPIRE via helm by running:
make spire-helm-install spire-wait-for-agent
If everything worked properly, you should see:
🏗️ Installing SPIRE using Helm...
NAME: spire-crds
LAST DEPLOYED: Thu Mar 7 15:07:12 2024
NAMESPACE: spire
STATUS: deployed
REVISION: 1
TEST SUITE: None
NAME: spire
LAST DEPLOYED: Thu Mar 7 15:07:13 2024
NAMESPACE: spire
STATUS: deployed
REVISION: 1
NOTES:
Installed spire…
Spire CR's will be handled only if className is set to "spire-spire"
✔️ SPIRE installed using Helm.
pod/spire-agent-qbc9q condition met
With SPIRE deployed and running on your cluster, let's analyze the SPIRE Server and Agent configuration files.
To first inspect the spire-server
config, run the following make
command:
make spire-view-server-config
The key differences from what we have seen before are shown below.
{
"server": {
"jwt_issuer": "127.0.0.1.nip.io"
},
"plugins": {
"KeyManager": [
{
"disk": {
"plugin_data": {
"keys_path": "/run/spire/data/keys.json"
}
}
}
]
}
}
- The Helm chart defaults to using the disk KeyManager
- We have configured the
jwt_issuer
field for the server
We have used 127.0.0.1.nip.io
as this will resolve to local host when we use curl to walk through the OIDC Discovery
protocol in Step 5: OIDC Discovery Walkthrough. nip.io is a
free service to map DNS names to any IP Address.
Next, view the running spire-agent
configuration by issuing:
make spire-view-agent-config
{
"plugins": {
"KeyManager": [
{
"disk": {
"plugin_data": {
"keys_path": "/run/spire/data/keys.json"
}
}
}
]
}
}
- The Helm chart defaults to using the disk KeyManager
The rest of the configuration should look familiar.
The SPIRE OIDC Discovery Provider exposes an endpoint that allows external services to verify JWT SVIDs provided by our SPIRE server. We will walk through how this works in the next section.
View the OIDC Discovery Provider configuration file by running:
make view-oidc-discovery-provider-config
{
"allow_insecure_scheme": true,
"domains": [
"spire-spiffe-oidc-discovery-provider",
"spire-spiffe-oidc-discovery-provider.spire",
"spire-spiffe-oidc-discovery-provider.spire.svc.cluster.local",
"127.0.0.1.nip.io"
],
"health_checks": {
"bind_port": "8008",
"live_path": "/live",
"ready_path": "/ready"
},
"listen_socket_path": "/run/spire/oidc-sockets/spire-oidc-server.sock",
"log_level": "info",
"workload_api": {
"socket_path": "/spiffe-workload-api/spire-agent.sock",
"trust_domain": "coastal-containers.example"
}
}
In the domains we can see that in addition to the configured JWT Issuer (127.0.0.1.nip.io
), the service also supports
the in cluster service DNS names.
To get a solid understanding of the OIDC Discovery protocol and how this can be used by external systems to verify JWT SVIDs, we'll manually walk through the process.
First we need to create a couple of registration entries; one for the SPIRE agent, and a second for a generic workload:
make create-registration-entries
Entry ID : caf1a7ab-a622-4d4c-bd4c-129081b72708
SPIFFE ID : spiffe://coastal-containers.example/agent/spire-agent
Parent ID : spiffe://coastal-containers.example/spire/server
Revision : 0
X509-SVID TTL : default
JWT-SVID TTL : default
Selector : k8s_psat:agent_ns:spire
Selector : k8s_psat:agent_sa:spire-agent
Selector : k8s_psat:cluster:kind-kind
Entry ID : de1fb77d-2586-45ae-b497-6f1552569271
SPIFFE ID : spiffe://coastal-containers.example/workload
Parent ID : spiffe://coastal-containers.example/agent/spire-agent
Revision : 0
X509-SVID TTL : default
JWT-SVID TTL : default
Selector : k8s:ns:default
Selector : k8s:sa:default
Next we'll generate get the SPIRE server to mint a JWT SVID directly using the cli (in practice workloads would do this through the Workload API) and then we will view the claims.
JWT_SVID=$(kubectl exec -n spire spire-server-0 -- \
bin/spire-server jwt mint -audience oidc-discovery -spiffeID spiffe://coastal-containers.example/workload)
jq -R 'split(".") | .[1] | @base64d | fromjson' <<< $JWT_SVID
We can see the token has:
- The requested audience:
oidc-discovery
- The validity period: from
iat
toexp
- The workload subject:
spiffe://coastal-containers.example/workload
- The token issuer:
https://127.0.0.1.nip.io
{
"aud": [
"oidc-discovery"
],
"exp": 1709883690,
"iat": 1709882790,
"iss": "127.0.0.1.nip.io",
"sub": "spiffe://coastal-containers.example/workload"
}
The OIDC Discovery Spec, section 4 states that:
OpenID Providers supporting Discovery MUST make a JSON document available at the path formed by concatenating the string /.well-known/openid-configuration to the Issuer
The Ingress for our cluster is using a NodePort mapped to 8443
on the host, so we can retrieve the OIDC Discovery
Document by running:
curl -sk https://127.0.0.1.nip.io:8443/.well-known/openid-configuration | jq
{
"issuer": "https://127.0.0.1.nip.io",
"jwks_uri": "https://127.0.0.1.nip.io/keys",
"authorization_endpoint": "",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [],
"id_token_signing_alg_values_supported": [
"RS256",
"ES256",
"ES384"
]
}
The Discovery Document tells us how to retrieve the keys we need to verify the signature contained in the JWT in the
jwks_uri
field.
curl -sk https://127.0.0.1.nip.io:8443/keys | jq
We can see this returns an array of keys
. As the SPIRE server rotates the signing keys, those that are still within
their validity period will appear in this list.
{
"keys": [
{
"kty": "RSA",
"kid": "OLDcvewt1bPTr5f33WXKXirPPUQbq0R8",
"alg": "RS256",
"n": "osfM5niRZzBL2pD6EVRHZOd0YwOo-BEyoT0rYJi1Fv0w1rjsda_objnixf_Nd4iQBLg2f1H3ttHkdBv3HMhqoKgbPV2Nd8yDhBcL2AhYVsyJLPyFvDnrEMA4jCRPs_52tjg_u9VgCk6OSrJGb1nhOIzzYOvtT2Zl90brMOTLIqHxtjDdtjJvL3a3t_JR_e_2bFq1hkzjSDF5y-B4acX4wtrvj9eEeTiCWYzlgLoi6wX2GwQ37t6Y1wwpvQ4qWIqFDL8-9yFVzqf0ZqDqmOnCHqbRHugawx5tB6ELuRi-PP-TaUxC0gN26j22Ysxzm10N4K6IXxqV4qf7ONvuHp6NTQ",
"e": "AQAB"
}
]
}
We can determine which key was used to sign our JWT from the headers
jq -R 'split(".") | .[0] | @base64d | fromjson' <<< $JWT_SVID
{
"alg": "RS256",
"kid": "OLDcvewt1bPTr5f33WXKXirPPUQbq0R8",
"typ": "JWT"
}
We can see that the kid
field in our JWT SVID: OLDcvewt1bPTr5f33WXKXirPPUQbq0R8
identifies the key contained in the
JWKS we retrieved, and we can use this to reconstitute the key, verify the signature and therefore the validation of the
presented JWT.
By exposing the OIDC Discovery Endpoint, we can present JWT SVIDs to any system that understands the OIDC Discovery
protocol, allowing them to verify our token and extract our identity from the claims (sub
) and the intended use for
the token (aud
) enabling identity federation. An example use case for this is using SPIRE provided JWT SVIDs along
with web identity federation to obtain temporary AWS
credentials.
As part of the helm deployment, we also installed the SPIRE Controller Manager a Kubernetes Controller that watches for workloads running in the cluster and automatically creates registration entries in the SPIRE server.
View the Controller Manager configuration file by running:
make view-controller-manager-config
apiVersion: spire.spiffe.io/v1alpha1
kind: ControllerManagerConfig
metadata:
name: spire-controller-manager
namespace: spire
labels:
helm.sh/chart: spire-server-0.1.0
app.kubernetes.io/name: server
app.kubernetes.io/instance: spire
app.kubernetes.io/version: "1.9.1"
app.kubernetes.io/managed-by: Helm
metrics:
bindAddress: 0.0.0.0:8082
health:
healthProbeBindAddress: 0.0.0.0:8083
leaderElection:
leaderElect: true
resourceName: 67103523.spiffe.io
resourceNamespace: spire
validatingWebhookConfigurationName: spire-spire-controller-manager-webhook
clusterName: kind-kind
trustDomain: coastal-containers.example
ignoreNamespaces:
- kube-system
- kube-public
- local-path-storage
spireServerSocketPath: "/tmp/spire-server/private/api.sock"
className: "spire-spire"
watchClassless: false
parentIDTemplate: "spiffe://{{ .TrustDomain }}/spire/agent/k8s_psat/{{ .ClusterName }}/{{ .NodeMeta.UID }}"
The key configuration item here is
parentIDTemplate: "spiffe://{{ .TrustDomain }}/spire/agent/k8s_psat/{{ .ClusterName }}/{{ .NodeMeta.UID }}"
which
templates the Parent ID for dynamic registration entries. As you can see, it creates an entry for the SPIRE agent for
the Node that the workload is deployed to.
The SPIFFE ID created for a workload is configured using the ClusterSPIFFEID custom resource. This can be viewed by running:
make view-spiffe-clusterid
{
"className": "spire-spire",
"namespaceSelector": {
"matchExpressions": [
{
"key": "kubernetes.io/metadata.name",
"operator": "NotIn",
"values": [
"spire",
"spire-server",
"spire-system"
]
}
]
},
"spiffeIDTemplate": "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
}
For workloads deployed to any namespace other than spire, spire-server, and spire-system, the SPIFFE ID will be of the
format spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}
. We will see this
in action in the next step where we deploy a workload, without creating a registration entry manually.
See the documentation for the SPIRE Controller Manager to understand how you can configure this to suit your requirements if the default configuration does not meet your needs.
In this step we repeat the previous steps programmatically and also provide an example of verifying the JWT SVID using the Go JOSE library.
Have a look at the workload and notice it performs the following steps:
- Create a Workload API client
- Obtain a JWT SVID (this is performed in a retry to allow time for the controller manager to register a SPIFFE ID for the workload)
- Print the raw JWT
- Parse the JWT and print the headers and claims
- Retrieve and print the OIDC Discovery document (we use
http://spire-spiffe-oidc-discovery-provider.spire
here so that it is resolvable from our workload in the cluster) - Extract the JWKS URI, download, and print the JWKS
- Verify the JWT SVID using the correct JWK and print the verified claims
To build and load the provided workload image into your Kind cluster and deploy the workload as a Job, run:
make cluster-build-load-image deploy-workload DIR=workload
Once the job has finished, run the following command to view the logs:
make view-logs
This will provide output similar to this below:
2024/03/08 09:13:22 JWT SVID
eyJhbGciOiJSUzI1NiIsImtpZCI6ImJ6UzRISGg0azcyWWY2UFNjUFdldFhVWXA3N3YxdHRHIiwidHlwIjoiSldUIn0.eyJhdWQiOlsib2lkYy1kaXNjb3ZlcnkiXSwiZXhwIjoxNzA5ODkwMTAyLCJpYXQiOjE3MDk4ODkyMDIsImlzcyI6IjEyNy4wLjAuMS5uaXAuaW8iLCJzdWIiOiJzcGlmZmU6Ly9jb2FzdGFsLWNvbnRhaW5lcnMuZXhhbXBsZS9ucy9kZWZhdWx0L3NhL3dvcmtsb2FkIn0.dQA4ngBL7C4HdeXg2Tbn2e83tnkWUSxq2cGor8K_Eu3x6Wv_ZMGyM7p0a6MCXq2QULUk-0hKcVO_PDi7Xub4zDAsAfxGx8_oLHbtPpYTVHrEsJWFih5GZWUFwk-0Q4CbPT8SapbU2xXX4ATAyEeLeRqSflsLTKJwVp5VaCdWgSFO0oWhWOmOMeZwrFmdqI-VOLJD5Xz6EUXh_XSpuaXq-sD7OHd0PgOLD1vrtGBclSinT-OQkmfkPDgyK7evk0zqoTVdLj86V3I4YRTM3XncP5eRHpHGsuAtiUluDBghGliNKEptWq5wbxwk89bgxyS7NA3UEzTQs4DemxSc9u7kPw
2024/03/08 09:13:22 Parsed Token
Headers:
alg: RS256
kid: bzS4HHh4k72Yf6PScPWetXUYp77v1ttG
Claims:
iss: 127.0.0.1.nip.io
sub: spiffe://coastal-containers.example/ns/default/sa/workload
aud: [oidc-discovery]
iat: 2024-03-08 09:13:22 +0000 UTC
exp: 2024-03-08 09:28:22 +0000 UTC
2024/03/08 09:13:22 OIDC Discovery Document from http://spire-spiffe-oidc-discovery-provider.spire/.well-known/openid-configuration:
{
"issuer": "http://spire-spiffe-oidc-discovery-provider.spire",
"jwks_uri": "http://spire-spiffe-oidc-discovery-provider.spire/keys",
"authorization_endpoint": "",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [],
"id_token_signing_alg_values_supported": [
"RS256",
"ES256",
"ES384"
]
}
2024/03/08 09:13:22 JSON Web Key Set: {
"keys": [
{
"kty": "RSA",
"kid": "bzS4HHh4k72Yf6PScPWetXUYp77v1ttG",
"alg": "RS256",
"n": "tl7rSTDhC9kWc00IRm9uBGYbDBPt56nkYYJGpZhSBDxTxsvjuQt-YnE17JKP3-rydgot_3bVqeBgOKyh7w8K-kj-nOndN8diGL4s9aS9Qz-hPpeZj2Mk-wFyeosSJH_ihxxeWLhvD2N3gXaDG5YTY5CFiy6-Iv4jkcrQ7t2m8B3bGCUpBQXy7bEeurOfVaWI8vqo7mjwBayblLZVCwx21stkyFaxhN2fsXeo74amS1ibkWYVb7LpHUPp9FuUM1bkKEz95r5aqIfQUDLa6Kd6SvvGyjgDWKlRVRFRKu4jcfDdwPgZUOcBJifls2dIiO24esTXtqqboew_mxbO8ga7iw",
"e": "AQAB"
}
]
}
2024/03/08 09:13:22 Verified claims:
{"aud":["oidc-discovery"],"exp":1709890102,"iat":1709889202,"iss":"127.0.0.1.nip.io","sub":"spiffe://coastal-containers.example/ns/default/sa/workload"}
To tear down the Kind cluster, run:
make cluster-down
As we dock our ship at the end of this thrilling voyage, we've successfully navigated the intricate waves of OpenID Connect (OIDC) Discovery and its integration with SPIRE. We've fortified the defenses of Coastal Containers Ltd., ensuring that our treasured cargo remains safe from modern-day digital pirates.
Throughout this lab, we've anchored our understanding of SPIRE's configuration, delved into the OIDC Discovery Provider's significance, and grasped the importance of the OIDC Discovery Document and JWKS. We've also witnessed the power of JWTs in action, ensuring secure communication between our workloads.
OIDC, with its robust set of features, provides a secure and scalable solution for authentication. By integrating it with SPIRE, we've taken a significant step towards a zero-trust security model, ensuring that only verified workloads can communicate within our environment.
As we set our sights on future adventures, let's remember the lessons learned on this journey. The seas of technology are vast and ever-changing, but with tools like SPIRE and OIDC, we're well-equipped to face any challenge that comes our way. Safe sailing, digital navigators! 🏴☠️🌊🔐