<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>credctl | Blog</title><description>Hardware-bound, short-lived cloud credentials.</description><link>https://credctl.com/</link><language>en</language><item><title>credctl Now Supports GCP: One Hardware Identity, Two Clouds</title><link>https://credctl.com/blog/credctl-now-supports-gcp/</link><guid isPermaLink="true">https://credctl.com/blog/credctl-now-supports-gcp/</guid><description>credctl auth --provider gcp exchanges the same hardware-bound JWT for short-lived GCP access tokens via Workload Identity Federation. Same Secure Enclave key. Same OIDC provider. No new infrastructure.

</description><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When we launched credctl, the pitch was simple: replace the plaintext AWS keys on your laptop with hardware-bound credentials using the Secure Enclave. No keys on disk. Ever.&lt;/p&gt;
&lt;p&gt;Today, we’re extending that to GCP.&lt;/p&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;credctl auth --provider gcp&lt;/code&gt; exchanges the same hardware-bound JWT for short-lived GCP access tokens via Workload Identity Federation. Same Secure Enclave key. Same OIDC provider. No new infrastructure. No GCP SDK dependency.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If you work across AWS and GCP — and an increasing number of teams do — you now have a single device identity that works with both clouds. One &lt;code dir=&quot;auto&quot;&gt;credctl init&lt;/code&gt;, one OIDC provider, two clouds.&lt;/p&gt;
&lt;p&gt;The security model is identical to AWS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Private key lives in the Secure Enclave. Can’t be exported.&lt;/li&gt;
&lt;li&gt;Credentials are short-lived (1 hour).&lt;/li&gt;
&lt;li&gt;Touch ID required for every signing operation.&lt;/li&gt;
&lt;li&gt;Nothing on disk to steal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The difference is in the exchange mechanism. AWS uses a single &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt; call. GCP uses a two-step exchange:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant CLI as credctl CLI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant SE as Secure Enclave&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant STS as GCP STS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant IAM as GCP IAM Credentials&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;SE: Sign JWT (Touch ID)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SE--&gt;&gt;CLI: Signed JWT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;STS: ExchangeToken (JWT)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS--&gt;&gt;CLI: Federated access token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;IAM: GenerateAccessToken&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IAM--&gt;&gt;CLI: OAuth2 access token (1h)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;The CLI signs a JWT with the Secure Enclave key (same as AWS, but with GCP’s Workload Identity Provider URI as the audience).&lt;/li&gt;
&lt;li&gt;GCP STS validates the JWT against the OIDC provider and returns a federated access token.&lt;/li&gt;
&lt;li&gt;The CLI exchanges the federated token for a service account access token via the IAM Credentials API.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The result: a standard GCP OAuth2 access token, usable with gcloud, Terraform, and all GCP client libraries.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;setup-five-minutes&quot;&gt;Setup: Five Minutes&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;You need credctl installed and initialised (&lt;code dir=&quot;auto&quot;&gt;credctl init&lt;/code&gt;), and AWS OIDC infrastructure deployed (&lt;code dir=&quot;auto&quot;&gt;credctl setup aws&lt;/code&gt;). GCP reuses the same OIDC provider — the CloudFront-hosted discovery documents that AWS validates are the same ones GCP validates.&lt;/p&gt;
&lt;p&gt;You also need a GCP service account with the roles you want credctl to assume.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;step-1-set-up-gcp-federation&quot;&gt;Step 1: Set up GCP federation&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;credctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setup&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gcp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--service-account&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;credctl@my-project.iam.gserviceaccount.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This single command handles everything: OIDC hosting on GCS, Workload Identity Pool, OIDC Provider, service account binding, and generating the credential config file at &lt;code dir=&quot;auto&quot;&gt;~/.credctl/gcp-credentials.json&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;step-2-use-it&quot;&gt;Step 2: Use it&lt;/h3&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;~&lt;/span&gt;&lt;span&gt;/.&lt;/span&gt;&lt;span&gt;credctl&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;gcp-credentials&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;gcloud&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;storage&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Or authenticate gcloud directly:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;gcloud&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auth&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--cred-file=&lt;/span&gt;&lt;span&gt;~&lt;/span&gt;&lt;span&gt;/.credctl/gcp-credentials.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s it. Every GCP API call now uses hardware-bound, short-lived credentials.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;terraform-for-teams&quot;&gt;Terraform for Teams&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;If your team prefers infrastructure-as-code over CLI setup, there’s a Terraform module at &lt;code dir=&quot;auto&quot;&gt;deploy/terraform-gcp/&lt;/code&gt;:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;module &lt;/span&gt;&lt;span&gt;&quot;credctl_gcp&quot;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;source &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;github.com/credctl/credctl//deploy/terraform-gcp&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;project_id         &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;my-project&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;device_fingerprint &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;SHA256:oKz3i9Tg...&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;issuer_url         &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://d1234.cloudfront.net&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;service_account_roles &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;roles/storage.objectViewer&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;roles/bigquery.dataViewer&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This creates the same resources as &lt;code dir=&quot;auto&quot;&gt;credctl setup gcp&lt;/code&gt; — Workload Identity Pool, OIDC Provider, Service Account, and IAM bindings — but version-controlled and repeatable.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;no-gcp-sdk&quot;&gt;No GCP SDK&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;One design decision worth highlighting: credctl has zero GCP SDK dependency. The GCP STS token exchange and IAM Credentials API calls are implemented with Go’s &lt;code dir=&quot;auto&quot;&gt;net/http&lt;/code&gt; stdlib. This keeps the binary small, the dependency tree minimal, and the attack surface narrow.&lt;/p&gt;
&lt;p&gt;The same is true for AWS — credctl’s only external Go dependency is &lt;a href=&quot;https://github.com/spf13/cobra&quot;&gt;spf13/cobra&lt;/a&gt; for CLI argument parsing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-the-oidc-architecture-made-this-easy&quot;&gt;How the OIDC Architecture Made This Easy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;When we chose OIDC federation over AWS Roles Anywhere (see &lt;a href=&quot;https://credctl.com/blog/secure-enclave-deep-dive/&quot;&gt;the technical deep dive&lt;/a&gt;), cross-cloud portability was a key reason. OIDC is a standard. AWS, GCP, and Azure all accept OIDC tokens for credential exchange. By building on OIDC, adding GCP support meant implementing the GCP-specific exchange protocol — not rebuilding identity infrastructure.&lt;/p&gt;
&lt;p&gt;The only code change to the shared JWT package was making the &lt;code dir=&quot;auto&quot;&gt;audience&lt;/code&gt; claim configurable. AWS uses &lt;code dir=&quot;auto&quot;&gt;sts.amazonaws.com&lt;/code&gt;. GCP uses the Workload Identity Provider URI. Same key, same signature, different audience.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Azure Federated Identity Credentials&lt;/strong&gt; — the third major cloud. Same OIDC architecture, Azure-specific exchange.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux TPM 2.0&lt;/strong&gt; — the same multi-cloud identity on Linux workstations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credential caching daemon&lt;/strong&gt; — avoid redundant STS calls and Touch ID prompts within the credential TTL. (This is already available as an experimental feature: &lt;code dir=&quot;auto&quot;&gt;credctl daemon start&lt;/code&gt;.)&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try It&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Update to the latest credctl:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;upgrade&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;credctl/tap/credctl&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GCP setup guide:&lt;/strong&gt; &lt;a href=&quot;https://credctl.com/guides/gcp-setup/&quot;&gt;credctl.com/guides/gcp-setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/credctl/credctl&quot;&gt;github.com/credctl/credctl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quickstart:&lt;/strong&gt; &lt;a href=&quot;https://credctl.com/quickstart/&quot;&gt;credctl.com/quickstart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re using credctl with both AWS and GCP, we’d love to hear about your experience. Open an issue or start a discussion on &lt;a href=&quot;https://github.com/credctl/credctl/discussions&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt; &lt;h3&gt;Stay up to date&lt;/h3&gt; &lt;p&gt;Get notified about new credctl releases, security best practices, and hardware identity insights.&lt;/p&gt; &lt;form action=&quot;https://buttondown.com/api/emails/embed-subscribe/credctl&quot; method=&quot;post&quot; target=&quot;popupwindow&quot;&gt; &lt;div&gt; &lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;you@example.com&quot; required aria-label=&quot;Email address&quot;&gt; &lt;button type=&quot;submit&quot;&gt;Subscribe&lt;/button&gt; &lt;/div&gt;  &lt;/form&gt; &lt;/div&gt; </content:encoded><category>gcp</category><category>security</category><category>credentials</category><category>multi-cloud</category></item><item><title>credctl: Eliminating Plaintext Cloud Keys with the Secure Enclave</title><link>https://credctl.com/blog/why-i-built-credctl/</link><guid isPermaLink="true">https://credctl.com/blog/why-i-built-credctl/</guid><description>86% of breaches involve stolen credentials. Your AWS keys sit in plaintext on disk. The Secure Enclave can fix this — credctl connects the pieces.

</description><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;the-problem-nobody-talks-about&quot;&gt;The Problem Nobody Talks About&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s a number that should keep you up at night: 86% of breaches involve stolen credentials. In 2025 alone, infostealer malware exfiltrated 1.8 billion credentials from 5.8 million devices — an 800% surge over recent years.&lt;/p&gt;
&lt;p&gt;And where are your cloud credentials right now?&lt;/p&gt;
&lt;p&gt;If you’re like most developers, the answer is &lt;code dir=&quot;auto&quot;&gt;~/.aws/credentials&lt;/code&gt;. A plaintext file. On your laptop. Readable by any process running as your user.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;[default]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aws_access_key_id = AKIAIOSFODNN7EXAMPLE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That file is the single most common credential pattern in cloud computing. It never expires. It works from any machine. And it can be stolen by any malware with file access, copied from disk images, committed to dotfile repos, or extracted from a stolen laptop.&lt;/p&gt;
&lt;p&gt;Think about what that means in practice. You install a VS Code extension, a Homebrew package, or a Python library — any of them could read &lt;code dir=&quot;auto&quot;&gt;~/.aws/credentials&lt;/code&gt; silently. You back up your disk, and those keys are in the backup. You sell your old laptop and don’t wipe it properly, and those keys go with it. You share your dotfiles on GitHub and accidentally include your credentials directory.&lt;/p&gt;
&lt;p&gt;The breach data tells the same story, over and over:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CircleCI (2023):&lt;/strong&gt; Malware on a developer laptop exfiltrated a session cookie. The attacker accessed production systems and forced all customers to rotate their secrets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LastPass (2022):&lt;/strong&gt; A developer workstation was compromised. Credentials from the first breach were used to access cloud storage containing customer vault backups. The result was one of the most catastrophic data breaches in password manager history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Okta (2023):&lt;/strong&gt; A stolen service account credential, stored in a personal Google account synced from a company device, gave attackers access to every customer’s support case files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uber (2022):&lt;/strong&gt; Stolen contractor credentials combined with MFA fatigue bombing compromised internal systems. The former CISO was criminally convicted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codecov (2021):&lt;/strong&gt; A modified CI script exfiltrated environment variables — including credentials — from thousands of downstream customers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every one of these traces back to the same pattern: long-lived credentials accessible on a developer machine. The attackers aren’t breaking encryption or exploiting zero-days. They’re reading files.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-gap-that-shouldnt-exist&quot;&gt;The Gap That Shouldn’t Exist&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s what’s strange. Your Mac has had a Secure Enclave since 2016. It’s a hardware security module — a dedicated chip that generates cryptographic keys, stores them in hardware, and never allows them to be exported. The private key physically cannot leave the chip. Not by malware, not by root access, not by forensic disk imaging.&lt;/p&gt;
&lt;p&gt;Your Mac uses the Secure Enclave for Touch ID, Apple Pay, and FileVault. Banks use it. Governments use it. Apple’s corecrypto module is FIPS 140-2 validated.&lt;/p&gt;
&lt;p&gt;Meanwhile, AWS has supported OIDC federation since 2014. Any service that can produce a signed JWT can exchange it for short-lived STS credentials via &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt;. No long-lived keys required.&lt;/p&gt;
&lt;p&gt;The Secure Enclave can sign JWTs. AWS can accept them. Nothing connected the two.&lt;/p&gt;
&lt;p&gt;That’s the gap credctl fills.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-credctl-does&quot;&gt;What credctl Does&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;credctl uses your Mac’s Secure Enclave to replace long-lived cloud access keys with short-lived, hardware-bound credentials. It works with both AWS and GCP today, with Azure on the roadmap.&lt;/p&gt;
&lt;p&gt;The private key lives in the Secure Enclave — it can never be exported, copied, or stolen. When you need AWS credentials, credctl signs a JWT with that hardware-bound key and exchanges it for short-lived STS credentials via OIDC federation. The credentials last one hour. There’s nothing on disk to steal.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ credctl init&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Generated ECDSA P-256 key pair in Secure Enclave&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Exported public key to ~/.credctl/device.pub&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Device ID: SHA256:oKz3i9Tg...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ credctl setup aws --policy-arn arn:aws:iam::123456789012:policy/DevAccess&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Deployed CloudFormation stack (S3, CloudFront, OIDC provider, IAM role)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ OIDC issuer URL: https://d1234.cloudfront.net&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ credctl auth&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Built JWT (iss=https://d1234.cloudfront.net, sub=SHA256:oKz3i9Tg...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Signed with Secure Enclave (Touch ID)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Exchanged for STS credentials (expires in 1h)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export AWS_ACCESS_KEY_ID=ASIA...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export AWS_SECRET_ACCESS_KEY=...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;export AWS_SESSION_TOKEN=...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That’s it. Five minutes from install to working credentials. No server infrastructure. No SSO portal. No browser redirects.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The architecture is straightforward:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant Dev as Developer&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant CLI as credctl CLI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant SE as Secure Enclave&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant STS as AWS STS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant OIDC as OIDC Provider&amp;#x3C;br/&gt;(S3 + CloudFront)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Dev-&gt;&gt;CLI: credctl auth&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;CLI: Build JWT (iss, sub, aud)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;SE: Sign JWT (ECDSA P-256 / ES256)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SE--&gt;&gt;CLI: Signed JWT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;STS: AssumeRoleWithWebIdentity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS-&gt;&gt;OIDC: Fetch JWKS (public key)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS-&gt;&gt;STS: Validate signature, assume role&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS--&gt;&gt;CLI: Short-lived credentials (1h)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI--&gt;&gt;Dev: AWS credentials&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;credctl init&lt;/code&gt;&lt;/strong&gt; generates an ECDSA P-256 key pair inside the Secure Enclave. The private key never leaves the hardware. The public key is exported.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;credctl setup aws&lt;/code&gt;&lt;/strong&gt; creates an S3 bucket to host OIDC discovery documents, an IAM OIDC identity provider that trusts the issuer URL, an IAM role configured for web identity federation, and configures the AWS CLI profile — all in a single command.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;credctl auth&lt;/code&gt;&lt;/strong&gt; builds a JWT with your issuer URL, device ID, and &lt;code dir=&quot;auto&quot;&gt;sts.amazonaws.com&lt;/code&gt; as the audience. The Secure Enclave signs it (Touch ID prompt). The CLI sends it to STS via &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt;. STS fetches your JWKS, validates the signature, and returns short-lived credentials.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The CLI is written in Go. The Secure Enclave integration uses cgo to call Apple’s Security framework directly (&lt;code dir=&quot;auto&quot;&gt;SecKeyCreateRandomKey&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;SecKeyCreateSignature&lt;/code&gt;). The STS call is a raw HTTP POST — no AWS SDK dependency. The only external dependency is spf13/cobra for CLI argument parsing.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;multi-cloud-aws-and-gcp-today&quot;&gt;Multi-Cloud: AWS and GCP Today&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;credctl’s OIDC architecture was designed for cross-cloud portability from the start. The same Secure Enclave key and OIDC provider work with both AWS and GCP.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GCP setup is just as simple:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ credctl setup gcp --service-account credctl@my-project.iam.gserviceaccount.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Created Workload Identity Pool&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Created OIDC Provider (using existing issuer URL)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Bound service account to device identity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ credctl setup gcp-cred-file&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;✓ Wrote credential config to ~/.credctl/gcp-credentials.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ export GOOGLE_APPLICATION_CREDENTIALS=~/.credctl/gcp-credentials.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$ gcloud storage ls&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;One hardware identity. Two clouds. No keys on disk for either.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-it-doesnt-do-yet&quot;&gt;What It Doesn’t Do (Yet)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;To be clear about scope — credctl today is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;macOS only.&lt;/strong&gt; Secure Enclave integration works on Apple Silicon (M1–M4) and Intel Macs with T2 chip. Linux TPM 2.0 support is planned but not yet implemented.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS and GCP.&lt;/strong&gt; Azure Federated Identity Credentials are on the roadmap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Individual use.&lt;/strong&gt; Direct Mode means each developer manages their own OIDC endpoint. A team broker with policy enforcement, audit trails, and device fleet management is planned for a future phase.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;credctl doesn’t replace HashiCorp Vault for secrets management. It replaces the plaintext cloud keys on your laptop. Different problem, different tool.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The roadmap expands in concentric circles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Azure:&lt;/strong&gt; Federated Identity Credentials, completing the three major clouds with one hardware identity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux TPM 2.0:&lt;/strong&gt; The same architecture using TPM 2.0 on Linux workstations. Windows 11 requires TPM 2.0, so the hardware is already there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credential broker for teams:&lt;/strong&gt; A centralised broker that sits between developers and cloud providers. Policy enforcement (who can assume which roles, from which devices), approval workflows, and a full audit trail mapped to SOC 2, NIS2, DORA, and ISO 27001 controls.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try It&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;credctl is open source (Apache 2.0).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/credctl/credctl&quot;&gt;github.com/credctl/credctl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quickstart:&lt;/strong&gt; &lt;a href=&quot;https://credctl.com/quickstart/&quot;&gt;Read the quickstart guide&lt;/a&gt; — install to working credentials in under 5 minutes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical deep dive:&lt;/strong&gt; &lt;a href=&quot;https://credctl.com/blog/secure-enclave-deep-dive/&quot;&gt;How credctl Uses the macOS Secure Enclave for Cloud Credentials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Questions and feedback welcome in &lt;a href=&quot;https://github.com/credctl/credctl/discussions&quot;&gt;GitHub Discussions&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt; &lt;h3&gt;Stay up to date&lt;/h3&gt; &lt;p&gt;Get notified about new credctl releases, security best practices, and hardware identity insights.&lt;/p&gt; &lt;form action=&quot;https://buttondown.com/api/emails/embed-subscribe/credctl&quot; method=&quot;post&quot; target=&quot;popupwindow&quot;&gt; &lt;div&gt; &lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;you@example.com&quot; required aria-label=&quot;Email address&quot;&gt; &lt;button type=&quot;submit&quot;&gt;Subscribe&lt;/button&gt; &lt;/div&gt; &lt;input type=&quot;hidden&quot; name=&quot;tag&quot; value=&quot;blog,launch&quot;&gt; &lt;/form&gt; &lt;/div&gt; </content:encoded><category>launch</category><category>security</category><category>credentials</category></item><item><title>How credctl Uses the macOS Secure Enclave for Cloud Credentials</title><link>https://credctl.com/blog/secure-enclave-deep-dive/</link><guid isPermaLink="true">https://credctl.com/blog/secure-enclave-deep-dive/</guid><description>A technical walkthrough of credctl&apos;s architecture — from Secure Enclave key generation through cgo, JWT construction, and OIDC federation with AWS STS.

</description><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This post explains the architecture of &lt;a href=&quot;https://github.com/credctl/credctl&quot;&gt;credctl&lt;/a&gt; — a CLI tool that replaces long-lived AWS access keys with short-lived, hardware-bound credentials using the macOS Secure Enclave. For the problem statement and product overview, read &lt;a href=&quot;https://credctl.com/blog/why-i-built-credctl/&quot;&gt;Eliminating Plaintext Cloud Keys&lt;/a&gt;. This post is the “how.”&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-the-secure-enclave-actually-is&quot;&gt;What the Secure Enclave Actually Is&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The Secure Enclave is a hardware security subsystem isolated from the main processor. On Apple Silicon Macs (M1–M4), it’s a dedicated core within the SoC. On Intel Macs with T2, it’s a separate chip. Either way, it has its own boot process, its own encrypted memory, and its own secure storage.&lt;/p&gt;
&lt;p&gt;What matters for credctl:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Key generation happens inside the hardware.&lt;/strong&gt; When you ask the Secure Enclave to create a key pair, the private key is generated inside the chip and never leaves it. There is no API to export it. There is no memory region to read it from. The hardware enforces this — it’s not a software policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signing happens inside the hardware.&lt;/strong&gt; You send data to the Secure Enclave, it signs it with the private key, and returns the signature. The private key is never exposed to the application, the operating system, or the CPU.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User presence verification.&lt;/strong&gt; The Secure Enclave gates key operations behind biometric (Touch ID) or passcode verification. Even with root access, a process cannot silently use a Secure Enclave key without the user’s explicit approval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FIPS 140-2 validated.&lt;/strong&gt; Apple’s corecrypto module, which underpins Secure Enclave operations, has FIPS 140-2 validation. This is the same level of certification used by government and financial systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Secure Enclave supports ECDSA with the P-256 curve (also known as secp256r1 or prime256v1). This maps directly to the ES256 algorithm in JWTs — which is exactly what credctl needs.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;credctls-architecture&quot;&gt;credctl’s Architecture&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;credctl is a Go CLI that bridges the Secure Enclave’s cryptographic capabilities to cloud provider credential APIs. The architecture has five components:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;graph TB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;subgraph &quot;credctl CLI&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CMD[Command Layer&amp;#x3C;br/&gt;cobra]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CFG[Config Manager&amp;#x3C;br/&gt;~/.credctl/config.json]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ENC[Enclave Interface&amp;#x3C;br/&gt;cgo → Security.framework]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JWT[JWT Builder&amp;#x3C;br/&gt;ES256 tokens]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS_C[AWS STS Client&amp;#x3C;br/&gt;AssumeRoleWithWebIdentity]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;subgraph &quot;Hardware&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SE[Secure Enclave&amp;#x3C;br/&gt;ECDSA P-256]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;subgraph &quot;AWS&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS[AWS STS]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;OIDC_P[OIDC Provider&amp;#x3C;br/&gt;S3 + CloudFront]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CMD --&gt; CFG&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CMD --&gt; ENC&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CMD --&gt; JWT&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CMD --&gt; STS_C&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ENC --&gt; SE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;JWT --&gt; ENC&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS_C --&gt; STS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS --&gt; OIDC_P&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;the-enclave-interface&quot;&gt;The Enclave Interface&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This is the critical piece. The Secure Enclave is accessed through Apple’s Security framework, which is an Objective-C/C API. Go doesn’t call C APIs natively, so credctl uses cgo — Go’s foreign function interface for C code.&lt;/p&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;Enclave&lt;/code&gt; interface abstracts hardware operations:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt; Enclave &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Available&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;bool&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;GenerateKey&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tag&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;ecdsa.PublicKey, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;LoadKey&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tag&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) (&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;ecdsa.PublicKey, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;DeleteKey&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tag&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Sign&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;tag&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;) ([]&lt;/span&gt;&lt;span&gt;byte&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;On macOS, the implementation calls Security framework functions via cgo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;GenerateKey&lt;/code&gt;&lt;/strong&gt; calls &lt;code dir=&quot;auto&quot;&gt;SecKeyCreateRandomKey&lt;/code&gt; with &lt;code dir=&quot;auto&quot;&gt;kSecAttrTokenIDSecureEnclave&lt;/code&gt; to create an ECDSA P-256 key pair inside the Secure Enclave. The key is tagged with a unique identifier so it can be loaded later. The function returns the public key — the private key stays in hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Sign&lt;/code&gt;&lt;/strong&gt; calls &lt;code dir=&quot;auto&quot;&gt;SecKeyCreateSignature&lt;/code&gt; with the &lt;code dir=&quot;auto&quot;&gt;kSecKeyAlgorithmECDSASignatureMessageX962SHA256&lt;/code&gt; algorithm. The Secure Enclave signs the data and returns the DER-encoded signature. By default (&lt;code dir=&quot;auto&quot;&gt;--biometric=any&lt;/code&gt;), this triggers a Touch ID prompt with passcode fallback. This behaviour is configurable via the &lt;code dir=&quot;auto&quot;&gt;--biometric&lt;/code&gt; flag at &lt;code dir=&quot;auto&quot;&gt;credctl init&lt;/code&gt; time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;LoadKey&lt;/code&gt;&lt;/strong&gt; retrieves a previously generated key by its tag using &lt;code dir=&quot;auto&quot;&gt;SecItemCopyMatching&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Build tags dispatch the implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;darwin.go&lt;/code&gt; — the real Secure Enclave implementation via cgo&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;other.go&lt;/code&gt; — a stub that returns clear errors on unsupported platforms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This cgo dependency is a deliberate architectural choice. It means credctl can’t be cross-compiled with a simple &lt;code dir=&quot;auto&quot;&gt;go build&lt;/code&gt; — it needs Xcode and an Apple provisioning profile. That’s a trade-off: harder distribution in exchange for real hardware security. The binary is distributed as a code-signed, notarised macOS app bundle.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;the-oidc-flow&quot;&gt;The OIDC Flow&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;This is where the Secure Enclave connects to AWS. The flow uses OIDC (OpenID Connect) federation — specifically, AWS STS &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt;. Here’s how the pieces fit together:&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;setup-one-time&quot;&gt;Setup (one-time)&lt;/h4&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;credctl init&lt;/code&gt;&lt;/strong&gt; generates the key pair in the Secure Enclave. It exports the public key to &lt;code dir=&quot;auto&quot;&gt;~/.credctl/device.pub&lt;/code&gt; and derives a device ID (SHA-256 fingerprint of the public key).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;credctl setup aws&lt;/code&gt;&lt;/strong&gt; deploys a CloudFormation stack that creates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An S3 bucket to host OIDC discovery documents&lt;/li&gt;
&lt;li&gt;A CloudFront distribution in front of S3 (AWS requires OIDC issuers to use HTTPS)&lt;/li&gt;
&lt;li&gt;An IAM OIDC identity provider that trusts the CloudFront URL as an OIDC issuer&lt;/li&gt;
&lt;li&gt;An IAM role with a trust policy that allows &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt; from the OIDC provider&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The command also generates and uploads the OIDC discovery documents (&lt;code dir=&quot;auto&quot;&gt;.well-known/openid-configuration&lt;/code&gt; and &lt;code dir=&quot;auto&quot;&gt;keys.json&lt;/code&gt; JWKS) automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After setup, you have a self-hosted OIDC provider backed by your device’s hardware identity. AWS trusts this provider because it’s registered as an IAM OIDC identity provider.&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;authentication-every-time&quot;&gt;Authentication (every time)&lt;/h4&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sequenceDiagram&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant CLI as credctl CLI&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant SE as Secure Enclave&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant STS as AWS STS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;participant CF as CloudFront&amp;#x3C;br/&gt;(OIDC Provider)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;CLI: Build JWT claims:&amp;#x3C;br/&gt;iss = CloudFront URL&amp;#x3C;br/&gt;sub = device ID (SHA-256)&amp;#x3C;br/&gt;aud = sts.amazonaws.com&amp;#x3C;br/&gt;iat = now&amp;#x3C;br/&gt;exp = now + 5min&amp;#x3C;br/&gt;jti = random UUID&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;SE: Sign JWT (ES256)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Note over SE: Touch ID prompt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SE--&gt;&gt;CLI: Signature&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;CLI: Assemble signed JWT&amp;#x3C;br/&gt;(header.payload.signature)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CLI-&gt;&gt;STS: AssumeRoleWithWebIdentity&amp;#x3C;br/&gt;(JWT + role ARN)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS-&gt;&gt;CF: GET /.well-known/openid-configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CF--&gt;&gt;STS: Discovery document&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS-&gt;&gt;CF: GET /keys.json&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CF--&gt;&gt;STS: JWKS (device public key)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS-&gt;&gt;STS: Verify JWT signature&amp;#x3C;br/&gt;against JWKS public key&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS-&gt;&gt;STS: Validate claims&amp;#x3C;br/&gt;(issuer, audience, expiry)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS-&gt;&gt;STS: Assume IAM role&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;STS--&gt;&gt;CLI: AccessKeyId +&amp;#x3C;br/&gt;SecretAccessKey +&amp;#x3C;br/&gt;SessionToken&amp;#x3C;br/&gt;(valid 1 hour)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;credctl auth&lt;/code&gt;&lt;/strong&gt; builds a JWT with these claims:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;iss&lt;/code&gt; — the CloudFront URL (OIDC issuer)&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;sub&lt;/code&gt; — the device ID (SHA-256 fingerprint of the public key)&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;aud&lt;/code&gt; — &lt;code dir=&quot;auto&quot;&gt;sts.amazonaws.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;iat&lt;/code&gt; — current timestamp&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;exp&lt;/code&gt; — current timestamp + 5 minutes (short-lived token)&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;jti&lt;/code&gt; — a random UUID (prevents replay)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The JWT header specifies &lt;code dir=&quot;auto&quot;&gt;alg: ES256&lt;/code&gt; and includes a &lt;code dir=&quot;auto&quot;&gt;kid&lt;/code&gt; (key ID) matching the key in the JWKS.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The CLI sends the JWT payload to the Secure Enclave for signing. With the default biometric policy (&lt;code dir=&quot;auto&quot;&gt;--biometric=any&lt;/code&gt;), this triggers a Touch ID prompt with passcode fallback. The Secure Enclave returns an ECDSA signature.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The CLI assembles the complete JWT (header.payload.signature, base64url-encoded) and calls STS &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt; with the JWT and the IAM role ARN.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;STS fetches the OIDC discovery document from CloudFront, follows it to the JWKS endpoint, retrieves the device’s public key, and validates the JWT signature.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If validation passes and the role’s trust policy allows it, STS returns temporary credentials — an access key ID, secret access key, and session token valid for one hour (default, configurable up to 12 hours).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&lt;h3 id=&quot;why-oidc-over-roles-anywhere&quot;&gt;Why OIDC Over Roles Anywhere&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;AWS offers two X.509-based credential mechanisms: &lt;strong&gt;OIDC federation&lt;/strong&gt; (via &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt;) and &lt;strong&gt;IAM Roles Anywhere&lt;/strong&gt; (via &lt;code dir=&quot;auto&quot;&gt;CreateSession&lt;/code&gt;). credctl uses OIDC federation. Here’s why:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Portability.&lt;/strong&gt; OIDC is a cross-cloud standard. GCP’s Workload Identity Federation and Azure’s Federated Identity Credentials both accept OIDC tokens. By building on OIDC, credctl’s architecture extends to multi-cloud without fundamental changes. Roles Anywhere is AWS-specific.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Simplicity.&lt;/strong&gt; OIDC federation requires hosting two static JSON files. Roles Anywhere requires managing X.509 certificates, certificate authorities, and certificate lifecycle. For a developer workstation tool, the OIDC approach is simpler.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JWT ecosystem.&lt;/strong&gt; OIDC uses JWTs, which are well-understood, easy to debug (paste into jwt.io), and have mature library support. X.509 certificate handling is more complex and error-prone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Existing infrastructure.&lt;/strong&gt; The Secure Enclave supports ECDSA P-256 signing, which maps directly to ES256 JWTs. No certificate generation or CA infrastructure needed.&lt;/p&gt;
&lt;p&gt;The trade-off: OIDC requires hosting a discovery endpoint (the S3 + CloudFront stack). Roles Anywhere doesn’t require any hosted infrastructure — it uses the X.509 certificate directly. For credctl’s use case, the simplicity and portability of OIDC outweigh the small infrastructure requirement.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;code-walkthrough-key-generation-to-credential-exchange&quot;&gt;Code Walkthrough: Key Generation to Credential Exchange&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Here’s the flow through credctl’s code, step by step.&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;step-1-key-generation-credctl-init&quot;&gt;Step 1: Key Generation (&lt;code dir=&quot;auto&quot;&gt;credctl init&lt;/code&gt;)&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;init&lt;/code&gt; command calls the Enclave interface to generate a key:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Enclave.GenerateKey(tag) → *ecdsa.PublicKey&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Under the hood (on macOS), this calls &lt;code dir=&quot;auto&quot;&gt;SecKeyCreateRandomKey&lt;/code&gt; with attributes specifying:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Token: &lt;code dir=&quot;auto&quot;&gt;kSecAttrTokenIDSecureEnclave&lt;/code&gt; (create in Secure Enclave, not software)&lt;/li&gt;
&lt;li&gt;Key type: &lt;code dir=&quot;auto&quot;&gt;kSecAttrKeyTypeECSECPrimeRandom&lt;/code&gt; (ECDSA)&lt;/li&gt;
&lt;li&gt;Key size: 256 bits (P-256 curve)&lt;/li&gt;
&lt;li&gt;Access control: determined by the &lt;code dir=&quot;auto&quot;&gt;--biometric&lt;/code&gt; flag (default: &lt;code dir=&quot;auto&quot;&gt;any&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code dir=&quot;auto&quot;&gt;--biometric&lt;/code&gt; flag controls the Secure Enclave access control policy for signing:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;code dir=&quot;auto&quot;&gt;--biometric&lt;/code&gt; value&lt;/th&gt;&lt;th&gt;Access control&lt;/th&gt;&lt;th&gt;Behaviour&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;any&lt;/code&gt; (default)&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;kSecAccessControlPrivateKeyUsage&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;kSecAccessControlUserPresence&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Touch ID with passcode fallback&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;fingerprint&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;kSecAccessControlPrivateKeyUsage&lt;/code&gt; + &lt;code dir=&quot;auto&quot;&gt;kSecAccessControlBiometryCurrentSet&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Touch ID only, no passcode fallback. Key is invalidated if fingerprints change.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;none&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;kSecAccessControlPrivateKeyUsage&lt;/code&gt; only&lt;/td&gt;&lt;td&gt;No user verification. Signing happens silently when the device is unlocked.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The biometric policy is set at key creation time and cannot be changed afterward. To change it, reinitialise with &lt;code dir=&quot;auto&quot;&gt;credctl init --force --biometric=&amp;#x3C;policy&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The public key is exported to &lt;code dir=&quot;auto&quot;&gt;~/.credctl/device.pub&lt;/code&gt; in PEM format. The device ID is the SHA-256 fingerprint of the public key bytes. The config is written to &lt;code dir=&quot;auto&quot;&gt;~/.credctl/config.json&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;step-2-jwt-construction-credctl-auth&quot;&gt;Step 2: JWT Construction (&lt;code dir=&quot;auto&quot;&gt;credctl auth&lt;/code&gt;)&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;The JWT builder constructs the token in three parts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Header:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;alg&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;ES256&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;typ&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;JWT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;kid&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;&amp;#x3C;key-id-matching-jwks&gt;&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Payload:&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;iss&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;https://d1234.cloudfront.net&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;sub&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sha256:a1b2c3d4e5f6...&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;aud&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sts.amazonaws.com&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;iat&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1709800000&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;exp&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1709800300&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;jti&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;550e8400-e29b-41d4-a716-446655440000&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The header and payload are base64url-encoded and concatenated with a dot: &lt;code dir=&quot;auto&quot;&gt;base64url(header).base64url(payload)&lt;/code&gt;. This becomes the signing input.&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;step-3-hardware-signing&quot;&gt;Step 3: Hardware Signing&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;The signing input bytes are sent to the Secure Enclave:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Enclave.Sign(tag, signingInput) → []byte (DER-encoded signature)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The Secure Enclave computes the ECDSA-SHA256 signature and returns it. The DER-encoded signature is converted to the raw &lt;code dir=&quot;auto&quot;&gt;r || s&lt;/code&gt; format that JWTs expect, then base64url-encoded.&lt;/p&gt;
&lt;p&gt;The final JWT: &lt;code dir=&quot;auto&quot;&gt;base64url(header).base64url(payload).base64url(signature)&lt;/code&gt;&lt;/p&gt;
&lt;div&gt;&lt;h4 id=&quot;step-4-credential-exchange&quot;&gt;Step 4: Credential Exchange&lt;/h4&gt;&lt;/div&gt;
&lt;p&gt;The CLI calls AWS STS &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt; via a raw HTTP POST (no AWS SDK):&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;POST https://sts.amazonaws.com/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Content-Type: application/x-www-form-urlencoded&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Action=AssumeRoleWithWebIdentity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x26;RoleArn=arn:aws:iam::123456789012:role/credctl-role&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x26;RoleSessionName=credctl-session&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x26;WebIdentityToken=&amp;#x3C;signed-jwt&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x26;Version=2011-06-15&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;STS validates the JWT by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetching &lt;code dir=&quot;auto&quot;&gt;https://d1234.cloudfront.net/.well-known/openid-configuration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Following the &lt;code dir=&quot;auto&quot;&gt;jwks_uri&lt;/code&gt; to &lt;code dir=&quot;auto&quot;&gt;https://d1234.cloudfront.net/keys.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Matching the &lt;code dir=&quot;auto&quot;&gt;kid&lt;/code&gt; in the JWT header to a key in the JWKS&lt;/li&gt;
&lt;li&gt;Verifying the ES256 signature against the public key&lt;/li&gt;
&lt;li&gt;Checking claims: issuer matches, audience is &lt;code dir=&quot;auto&quot;&gt;sts.amazonaws.com&lt;/code&gt;, token is not expired&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If valid, STS assumes the IAM role and returns temporary credentials.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;security-properties&quot;&gt;Security Properties&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;credctl’s architecture provides several security guarantees:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Private key never leaves hardware.&lt;/strong&gt; The Secure Enclave enforces non-exportability at the hardware level. Even with root access, an attacker cannot read the private key. They would need physical possession of the specific device AND the user’s biometric (or passcode) to sign a JWT.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Short-lived credentials.&lt;/strong&gt; The STS credentials returned by &lt;code dir=&quot;auto&quot;&gt;AssumeRoleWithWebIdentity&lt;/code&gt; are temporary — one hour by default, configurable up to 12 hours. There are no long-lived credentials on disk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Replay protection.&lt;/strong&gt; Each JWT includes a unique &lt;code dir=&quot;auto&quot;&gt;jti&lt;/code&gt; (JWT ID) and a short &lt;code dir=&quot;auto&quot;&gt;exp&lt;/code&gt; (5 minutes). Even if a JWT were intercepted, it would expire before it could be meaningfully reused.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User presence required (default).&lt;/strong&gt; With the default biometric policy (&lt;code dir=&quot;auto&quot;&gt;--biometric=any&lt;/code&gt;), every signing operation requires Touch ID or passcode verification. Malware cannot silently use the Secure Enclave key to obtain credentials. This can be disabled with &lt;code dir=&quot;auto&quot;&gt;--biometric=none&lt;/code&gt; at &lt;code dir=&quot;auto&quot;&gt;credctl init&lt;/code&gt; time for headless environments, at the cost of losing user-presence protection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Revocation via JWKS.&lt;/strong&gt; To revoke a device’s access, remove its public key from the JWKS and delete the IAM OIDC identity provider trust. STS will no longer validate JWTs from that device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No credential caching on disk.&lt;/strong&gt; credctl does not cache STS credentials to disk in plaintext. The credentials are output to stdout or set as environment variables for the current session.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;what-this-doesnt-protect-against&quot;&gt;What This Doesn’t Protect Against&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;No security architecture is complete without stating its limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A compromised device with the user present.&lt;/strong&gt; If an attacker has remote access to a device and the user approves a Touch ID prompt (social engineering or confusion), the attacker gets valid short-lived credentials. The mitigation is that the credentials are short-lived and the attack window is narrow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credential misuse after issuance.&lt;/strong&gt; Once STS credentials are issued, they’re standard AWS temporary credentials. If they’re logged, written to a file, or used in a shared terminal, they can be used by anyone until they expire.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OIDC provider compromise.&lt;/strong&gt; If an attacker gains write access to the S3 bucket hosting the JWKS, they could add their own public key and sign JWTs that STS would accept. The CloudFormation stack configures bucket access controls, but the OIDC provider is a critical trust boundary. The brokered mode (planned) eliminates this risk by centralising OIDC management.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h3 id=&quot;comparison-to-alternative-approaches&quot;&gt;Comparison to Alternative Approaches&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;It’s worth comparing credctl’s security properties to common alternatives:&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Approach&lt;/th&gt;&lt;th&gt;Key Storage&lt;/th&gt;&lt;th&gt;Exportable?&lt;/th&gt;&lt;th&gt;Credential Lifetime&lt;/th&gt;&lt;th&gt;Device Binding&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;~/.aws/credentials&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Plaintext file&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Permanent&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1Password Shell Plugins&lt;/td&gt;&lt;td&gt;Encrypted software vault&lt;/td&gt;&lt;td&gt;Yes (if vault decrypted)&lt;/td&gt;&lt;td&gt;Permanent (stored key)&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AWS Identity Center&lt;/td&gt;&lt;td&gt;Browser session cookie&lt;/td&gt;&lt;td&gt;Yes (cookie theft)&lt;/td&gt;&lt;td&gt;8–12 hours&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;credctl&lt;/td&gt;&lt;td&gt;Secure Enclave hardware&lt;/td&gt;&lt;td&gt;No (hardware-enforced)&lt;/td&gt;&lt;td&gt;1 hour (STS)&lt;/td&gt;&lt;td&gt;Hardware-bound&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The fundamental difference is the transition from software-stored credentials (which can be copied) to hardware-bound credentials (which physically cannot be copied). This is not an incremental improvement — it’s an architectural change in where trust is anchored.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;performance&quot;&gt;Performance&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Credential issuance involves three operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JWT construction:&lt;/strong&gt; Microseconds. String formatting and base64 encoding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure Enclave signing:&lt;/strong&gt; Typically 20–50ms for the cryptographic operation, plus Touch ID interaction time (user-dependent).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;STS API call:&lt;/strong&gt; 100–300ms depending on network latency.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Total time from &lt;code dir=&quot;auto&quot;&gt;credctl auth&lt;/code&gt; to credentials: under one second of compute, plus Touch ID interaction. This is faster than browser-based SSO flows, which typically require 5–15 seconds of page loads, redirects, and MFA prompts.&lt;/p&gt;
&lt;p&gt;STS caches the OIDC discovery document and JWKS, so subsequent credential requests in the same session don’t require CloudFront fetches. The signing operation itself is constant-time — the Secure Enclave processes ECDSA signatures at the same speed regardless of the data being signed.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;The architecture is designed to extend in two directions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Platform expansion.&lt;/strong&gt; The &lt;code dir=&quot;auto&quot;&gt;Enclave&lt;/code&gt; interface abstracts hardware operations, so adding Linux TPM 2.0 support means implementing the same interface against &lt;code dir=&quot;auto&quot;&gt;go-tpm&lt;/code&gt; instead of Apple’s Security framework. The JWT builder, OIDC generator, and STS client remain unchanged. The same applies to Windows TPM via a Windows-specific build.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-cloud.&lt;/strong&gt; The OIDC-based architecture was chosen specifically because GCP Workload Identity Federation and Azure Federated Identity Credentials accept OIDC tokens. Adding GCP support means implementing a GCP token exchange client; adding Azure means implementing an Azure AD token exchange client. The hardware identity, JWT construction, and OIDC provider infrastructure remain the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Brokered mode.&lt;/strong&gt; For teams, the CLI will authenticate to a centralised credctl broker instead of directly to cloud providers. The broker verifies device identity, evaluates policy (who can assume which roles from which devices), brokers credentials from cloud providers, and logs everything for audit. The hardware identity foundation is the same — the broker trusts the device’s signed assertion because it has the device’s public key in its registry.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/credctl/credctl&quot;&gt;credctl GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://credctl.com/quickstart/&quot;&gt;Quickstart guide&lt;/a&gt; — install to working credentials in 5 minutes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.apple.com/guide/security/secure-enclave-sec59b0b31ff/web&quot;&gt;Apple Secure Enclave documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html&quot;&gt;AWS AssumeRoleWithWebIdentity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openid.net/specs/openid-connect-core-1_0.html&quot;&gt;OIDC Core specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;h3&gt;Stay up to date&lt;/h3&gt; &lt;p&gt;Get notified about new credctl releases, security best practices, and hardware identity insights.&lt;/p&gt; &lt;form action=&quot;https://buttondown.com/api/emails/embed-subscribe/credctl&quot; method=&quot;post&quot; target=&quot;popupwindow&quot;&gt; &lt;div&gt; &lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;you@example.com&quot; required aria-label=&quot;Email address&quot;&gt; &lt;button type=&quot;submit&quot;&gt;Subscribe&lt;/button&gt; &lt;/div&gt; &lt;input type=&quot;hidden&quot; name=&quot;tag&quot; value=&quot;blog,technical&quot;&gt; &lt;/form&gt; &lt;/div&gt; </content:encoded><category>architecture</category><category>secure-enclave</category><category>deep-dive</category></item></channel></rss>