The application connects to MySQL without a password. Not “the password is in a vault.” Not “the password is injected at deploy time.” Not “no password needed because the request comes from an allow-listed IP.” No password at all. There is no string, anywhere, that logs into that database.

That sentence is where I recently lost a friend, a senior engineer at one of the biggest tech companies you can name, while explaining my fleet-security work. He made me say it three times. And he is not behind; he is normal. Zero-trust workload identity is still a novelty to most people who run real infrastructure, including very good ones.

So I built a small, honest demo on one of my lab machines, and this article walks through it step by step. I kept it deliberately simple, on the most classic stack the Internet ever ran: a PHP application connecting to a MySQL server. No service mesh, no sidecar chorus, no platform team required. If the pattern holds there, it holds anywhere. It is the first in a series: this part establishes what workload identity is; the next part will layer network-access rules on top of it.

The world we all grew up in: secrets sprawl and shared passwords

Here is the system we are replacing, and I will not caricature it, because I have run it for years and so have you. Follow one production MySQL password around a real company. It starts in the ODBC DSN of every Windows workstation of a legacy ERP, configured by hand, machine by machine. It is also in the Tomcat context.xml of the app server, in the saved connection of that satellite BI box nobody quite owns, in the FP&A analyst’s Excel workbook that queries SQL directly, in a Kubernetes Secret faithfully replicated to five clusters across providers and regions, and in the -p flag of the 2 a.m. backup cron. All of them, the same string:

THE OLD WORLDOne password, glued into every systemWindows · ODBC DSNWS-ACCT-07Driver MySQL ODBC 5.3User erp_appPassword ••••••••Sup3rS3cret!2019Tomcat · context.xmlerp-app01<Resource name="jdbc/erp"username="erp_app"password= ⤵Sup3rS3cret!2019BI · saved connectionbi-sat-01datasource.propertiesjdbc.user=erp_appjdbc.pass= ⤵Sup3rS3cret!2019Excel · FP&A workbookLAPTOP-FPA-02forecast_Q3.xlsxProvider=MSDASQL;Uid=erp_app;Pwd= ⤵Sup3rS3cret!2019k8s · Secret ×5 clustersaws·gcp·azure·…kind: Secretdata:db-pass: ⤵Sup3rS3cret!2019cron · nightly backupbackup-010 2 * * * mysqldump \-u erp_app \-p ⤵Sup3rS3cret!2019…plus the wiki, the ticket queue, the onboarding doc, and everywhere a human ever pasted itThe one production MySQLGRANT ... TO 'erp_app' IDENTIFIED BYSup3rS3cret!2019← presented on every handshake, from everywhereROTATION DAYChange it on the server, and everything above breaks at once: six systems, five owners.IT owns the workstations, middleware owns Tomcat, nobody owns the BI box, finance owns the workbook. So: never.THE SNAPSHOT PROBLEMAny one of these, snapshotted once, leaks the key to everything, forever.A VM image, a container layer, a workbook in file-sync, an old backup: the attacker works offline, with allthe time in the world, against a credential that never expires and is honored from any network on earth.

The application proves who it is by knowing a string. That has four properties that hurt:

  1. The string is the identity. Any process, any laptop, any leaked backup that has it is the app, from the database’s point of view.
  2. It never expires on its own. A password leaked today is valid until someone notices.
  3. Rotation is a cross-team project. The copies live in systems with different owners: IT’s workstations, middleware’s Tomcat, the orphaned BI box, finance’s workbook, the platform team’s five clusters. Changing the string means all of them, in the right order, without an outage. Which is why it happens roughly never.
  4. It travels. Every connection from every region presents it in the handshake, and every snapshot, backup, or synced file that ever contained it is a time capsule: an attacker who obtains one works offline, at leisure, against a credential that will still be valid next year.

Vaults help with sprawl, and they are a genuine improvement. But note what a vault actually does: it centralizes storage of the string. The application still fetches the same long-lived secret and presents it; the database still can’t tell the app from anyone else holding the string. The identity problem is untouched.

What workload identity actually is: SPIFFE, SPIRE, and attestation

Workload identity flips the question. Instead of “does this connection know the secret?” the system asks “what, verifiably, IS this workload?”, and issues it a short-lived cryptographic identity based on the answer.

Before the vocabulary, one structural point, because it is where readers who already know some identity tend to pattern-match wrong. Every authentication scheme you grew up with is a private transaction between two parties. A user on a Linux box has a password and a keypair; the client connects, the server challenges, the client answers, done. Peer to peer. MySQL accounts, SSH keys, API tokens, even mTLS with certificates you provisioned by hand: different cryptography, same two-party shape. Whatever the client presents, the client has to hold.

That shape is also behind the worst definition of workload identity I keep running into. A few years ago I watched engineers describe it as “embedding the credential in the program itself, compiled in, so no other place has it.” Wrong. That is not removing the luggage tag with your name and phone number; that is moving it inside the bag. The secret is still there, hardcoded now, still walking through the network on every handshake, still sitting in every snapshot of the image.

Workload identity breaks the two-party shape. There is a third element in the room: an arbiter. It can live on the same host, as in this demo. It can sit at your network perimeter, serve a private CIDR, or run on the public Internet; Intel and NVIDIA both operate public-facing attestation services today. The arbiter does one job: it evaluates evidence against rules you wrote, and ships unique, custom-made, time-sensitive credentials. Think of a Let’s Encrypt-style issuer, except not for websites and not on a ninety-day clock. Yours, on your rules, down to minutes.

And the rules read like operational judgment, not cryptography. “This release was audited, I trust it.” “This machine is mine, I trust it.” “This VPC is my gated prod; whatever I published inside it, I trust.” You choose how long the trust holds: a day, an hour, fifteen minutes. From there the client and the server handle issuance and renewal between themselves, hassle-free; certificates rotate and nobody files a ticket. Change your mind and one central action revokes across the whole fleet.

You compose these rules with AND and OR, as many clauses as your posture needs. The demo below is two clauses joined by AND, on a classic MySQL server: voucher one specific container image, running on my attested machine. That image is granted access automatically. Any other image, no.

The open-source standard for this is SPIFFE, and its reference implementation is SPIRE, whose server plays the arbiter role you just met. Four plain-words definitions and you know the whole vocabulary:

  • A SPIFFE ID is a name, like spiffe://demo.ure.us/workload/webapp. Just a name.
  • An SVID is that name carried inside a short-lived X.509 certificate: the same kind that secures every HTTPS site, so every database and proxy you already run knows how to verify one.
  • Attestation is how the name gets issued: not to whoever asks, but to whatever can be verified by checking evidence, not secrets.
  • The Workload API is a local socket where a workload asks “who am I?” It presents nothing. It doesn’t need a bootstrap secret. The system observes what the caller is and decides what it gets.

Trust is built in two layers, and the whole thing is a short conversation:

THE NEW WORLDIdentity issued to verified propertiesRead it top to bottom. It is a conversation, and time flows down.asksgrantsThe arbiterSPIRE server · your rulesThe machineai02 · agent + TPM chipThe workloadphp:8.3-apache containerThe databaseMySQL · trusts SPIRE CAYOUR RULEhost = ai02AND image = sha256:c5b7…-> webapp · 15 minutesprove the machine:the TPM signs a fresh challenge(once; the key never leaves the chip)trusted: you are node ai02AUTO-RENEW LOOP · EVERY ~7 MIN"who am I?" (presents nothing)runtime check: which container,from exactly which image? ✓ ruleSVID: a 15-minute certificatefor workload/webappmTLS: the certificate IS the loginchain ✓ subject ✓ password: noneNothing to steal that outlives 15 minutes.Identity goes to verified properties, this machine and this exact image, not to whoever holds a string. Renewal is the loop above.

First the machine proves itself to the identity authority. The SPIRE server challenges it with a fresh, never-repeated number, and the machine signs it, in this demo with a key that lives inside my server’s TPM chip (clouds do the same dance with their instance-identity documents). Only then does the agent on that machine get to attest workloads.

Two sentences on TPMs for anyone who has never met one. Every business-class motherboard ships with this tamper-resistant chip, and at the factory it receives an Endorsement Key. Think of a birth certificate engraved inside the silicon: the chip maker signs its certificate (mine says Nuvoton Technology Corporation, read straight out of the chip during the demo), and the private half physically cannot leave the die. For daily work the chip mints working keys, our DevID, best understood as a signature stamp locked inside a sealed box with a slot: the machine can use the stamp to sign challenges, but it can never see or copy it. Steal every file on the machine and its identity stays behind. Not tamperable, not forgeable, not extractable, not spoofable: that is the property the whole fleet stands on.

And the rule that turns all of that hardware ceremony into policy is almost embarrassingly small. This is the entire machine-trust configuration of the demo’s identity server, whole, with only the file paths shortened:

NodeAttestor "tpm_devid" {
    plugin_data {
        devid_ca_path       = "pki/enroll-ca.crt"              # my blessing
        endorsement_ca_path = "pki/endorsement-ca-bundle.pem"  # the chip maker's chain
    }
}

Two file paths: the certificate authority I used to bless the machine’s chip-resident key, and the chip maker’s public chain proving the silicon is genuine. And when a process asks the agent for an identity, the agent doesn’t take its word for anything. It asks the container runtime: which container is this, created from exactly which image? An image digest is a cryptographic address: change one byte in the image and the digest changes.

So the policy, the entire policy, reads like this: on this proven host, the container running exactly these bytes is spiffe://demo.ure.us/workload/webapp, for 15 minutes at a time.

The demo design: image attestation AND TPM machine attestation

Before walking through the build, here is the entire trust design of the demo, stated the way I would put it on a whiteboard. Everything the database will ever trust reduces to two questions:

DEMO DESIGNTwo checks. One declaration. Zero secrets.GATE 1 · THE BLESSED IMAGEExactly one image may hold this identity.You name the version; you pin the bytes.docker:image_config_digest:sha256:c5b70331325a…a public image, or your own signed build;CI/CD promotes a release by updating one digestGATE 2 · A MACHINE OF MY FLEETThe host signs with a key inside its TPM chip.Hardware attestation: not a file, not a secret.tpm_devid:subject:cn:ai02.demo.ure.uscannot be tampered with, forged,extracted, or spoofedThe whole policy is one central declaration, and it covers the entire fleet:spire-server entry create \-spiffeID spiffe://demo.ure.us/workload/webapp \-parentID spiffe://demo.ure.us/node/ai02 \← gate 2: my fleet-selector docker:image_config_digest:sha256:c5b7…← gate 1: my imageIs this the image I blessed? Yes. Is it on a machine of my fleet? Yes. → Connect.Anything else that asks for an identity receives PermissionDenied: no identity issued.No password was created, stored, injected, or rotated to make any of this true.

Sit with the first gate for a second, because it is where a development team’s state of mind changes. The image can be a stock public one, as in this demo, or your own hardened build. The point is that you specify the gate: this version, with this exact sha256, is the one I bless. Nobody writes credential-handling code. Nobody wires a secrets manager into the app. Nobody remembers, at 2 a.m., which environment file the staging password lives in. The whole fleet’s access policy is that one declaration, and your CI/CD pipeline becomes the control point: promoting a release is updating the blessed digest. Take control of that workflow and you have eliminated one of the biggest standing worries in distributed systems.

I will tell you when this landed for me personally. At a previous job, every time a team member left, we manually rotated hundreds of passwords: days of coordinated toil, spread across teams, every single departure, because any string a person might have seen had to be presumed carried out the door. Read the declaration above again: there is nothing in it a departing person could take. The identity is the machine and the image, not a string in someone’s head or home directory. The offboarding checklist for this database is: nothing.

The build: passwordless MySQL with mTLS and 15-minute certificates

On my lab host I ran the whole thing with rootless podman and two unmodified public images: mysql:8.4 and php:8.3-apache. No custom builds: the point is that identity attaches to the exact public image, not to something I baked. The SPIRE server issues 15-minute workload certificates. Registration is two commands:

spire-server entry create -parentID spiffe://demo.ure.us/node/ai02 \
  -spiffeID spiffe://demo.ure.us/workload/mysql \
  -selector docker:image_config_digest:sha256:083c8ddb1fa1... \
  -dns mysql.wi-demo

spire-server entry create -parentID spiffe://demo.ure.us/node/ai02 \
  -spiffeID spiffe://demo.ure.us/workload/webapp \
  -selector docker:image_config_digest:sha256:c5b70331325a... \
  -dns webapp.wi-demo

Each container starts through a small fetch-then-exec wrapper that first fetches its own SVID over the Workload API (the fetch runs inside the container, so the attestor sees the pinned image digest) and then hands off to the stock entrypoint. MySQL comes up with its certificate files already in place, TLS mandatory, and one crucial setting: the CA it trusts for client certificates is the SPIRE trust bundle. From that moment, only members of the trust domain can even present a credential to it.

Then the punchline: the webapp’s certificate subject is stable across every rotation (CN=webapp.wi-demo comes from the registration entry), so the entire auth schema for the application is:

CREATE USER 'webapp'@'%'
  REQUIRE SUBJECT '/C=US/O=SPIRE/CN=webapp.wi-demo'
  AND ISSUER '/C=US/O=URE Demo/CN=URE Workload Identity Demo CA/...';
GRANT SELECT ON demo.* TO 'webapp'@'%';

Read it out loud: there is no password in that CREATE USER. The account is reachable only over TLS, only with a certificate that chains to the SPIRE CA, whose subject is exactly this workload’s name. The certificate, issued to a proven host running exact bytes and expiring in 15 minutes, is the credential. And the PHP side is one mysqli_ssl_set() call pointing at the certificate files, with password '':

The demo page, every gate green: TPM host attestation PASS, image attestation PASS, identity issuance PASS, mutual TLS PASS, passwords NONE; connected as webapp@% with no password

The page re-reads the certificate files on every load, and the agent replaces them automatically at about half the TTL. Which means credential rotation, the thing that was a project in the old world, costs this application literally nothing. No restart, no reload hook, no code. I watched the serial number change mid-demo while writing this article; the app never noticed.

Zero trust in practice: what the system refuses

A security demo that only shows the happy path is a toy. The refusals are where the model earns its keep:

Plaintext, with the correct admin password. Even root, with the right password, over a non-TLS connection, with the client explicitly configured to attempt it: ERROR 3159: Connections using insecure transport are prohibited. Right string, wrong transport, no entry. (Left to its own defaults, the client refuses even earlier: caching_sha2_password will not send credentials over plaintext.)

TLS, no certificate. Connecting as webapp over TLS but without a client certificate: ERROR 1045: Access denied. There is no password to guess. The account has none. Brute force has no target.

The imposter. This is the beat that landed hardest with my friend. I started php:8.2-apache (one minor version off, a perfectly legitimate public image) with the identical application code, on the same host and network, and had it ask for an identity. SPIRE’s answer, verbatim:

PermissionDenied desc = no identity issued

The imposter page: image attestation NOT PROVEN with the digest mismatch shown, identity issuance REFUSED quoting PermissionDenied, not connected

Same code, same host, same network. Wrong bytes. It never even gets far enough to fail the MySQL handshake, because it has no credential to fail with. In the old world, this container would have worked fine: it would have read the same DB_PASSWORD from the same environment and the database would have been none the wiser.

And the kill switch. Delete the registration entry, one command at the central server, and new issuance stops within seconds. Honesty requires the next sentence too: the certificate already in the workload’s hands keeps working until it expires. Classic PKI would hand that residue to a revocation list, and the knob exists on this exact stack: MySQL accepts a CRL (--ssl-crl) and hot-reloads it, so an on-the-fly deny list is buildable here. This demo’s CA ships none, by design. When SPIRE’s maintainers scoped their revocation work, they ruled CRLs and deny lists out in writing and built forced rotation instead; their reasoning, in their own words, is that rotating away from a compromised key “exercises existing pathways that must be working,” while a distrust list is a second distribution system, one that gets exercised only on the worst day: stale copies, validators that fail open, one more dependency sitting between you and containment. Fifteen-minute lifetimes buy the same containment with machinery you already run, and the emergency lever that remains is revoking an entire signing authority through the trust bundle, a story for another article. My demo measured a 764-second maximum exposure window, after which the workload went dark with zero cleanup actions.

Passwords vs workload identity, side by side

Password authWorkload identity
What proves identityKnowing a stringVerified properties: attested host + exact image
Who can hold itAnything that ever saw the stringOnly the attested workload, re-verified at issuance
LifetimeUntil someone rotates it (≈ never)15 minutes, auto-renewed
Rotation costFind every copy, coordinate, prayZero: the app re-reads files
Leak blast radiusFull access until noticed + rotated≤ TTL, then self-extinguishing
RevocationRotate everywhere, redeployDelete one central entry
What the DB storesA password hashREQUIRE SUBJECT: a name, no secret

What the demo simplifies (and what production SPIRE does instead)

An introduction that hides its simplifications teaches the wrong thing, so, plainly:

  • The machine gate is real hardware. The demo attests through my server’s actual TPM (tpm_devid), the same mechanism my production fleet uses. What is demo-grade is the enrollment: the CA that “blesses” the chip’s key into a name is a local throwaway, where production runs a controlled enrollment ceremony. Who should hold those roots is its own subject: Who Holds the Keys to Confidential Computing.
  • A digest pin is not a signature. image_config_digest says “exactly these bytes,” not “bytes someone endorsed.” In a real fleet you verify the publisher’s cosign signature and pin the digest it resolves to; they compose.
  • Image trust attests what the container was created from, not what it does at runtime. My wrapper compiles a PHP extension inside the running container and the digest doesn’t change. Know what the selector does and doesn’t claim.
  • MySQL was the deliberate brick-and-mortar choice, and it does not speak SPIFFE. Nothing in mysqld knows the Workload API exists. The rotation is bolted on from outside (my wrapper loop; a crontab would do), and “no password” is MySQL’s idiom for it: an empty-credential account gated by REQUIRE SUBJECT ... AND ISSUER. PostgreSQL treats certificate identity as a first-class login method (clientcert=verify-full with a certificate-to-role map, config reloaded on a plain SIGHUP); if you’re Postgres-shaped, the story is cleaner end to end. That is the point of the choice: if the pattern retrofits onto a database that never heard of it, it retrofits onto your estate. One sharp edge my demo caught live: SPIRE embeds a per-generation serial in its CA subject, so the REQUIRE ISSUER string changes when the CA rotates. Derive it from the live certificate; never hardcode it.
  • My few-line fetch-and-refresh loop is a stand-in for spiffe-helper, which automates exactly this in production.
  • The server’s identity rots too; I proved it by accident. I left the demo running past MySQL’s 1-hour certificate with nothing refreshing it, and clients started refusing the server: Cannot connect to MySQL using SSL. Short-lived certificates are a discipline, not a client-side feature. The fix is the production path: periodic re-fetch plus ALTER INSTANCE RELOAD TLS, which hot-reloads mysqld’s certificate files with no restart and no dropped connections. The demo’s wrapper now does this every 20 minutes.

Post-quantum cryptography (PQC): readiness is a rotation problem

One more property of this architecture deserves its own section, and it has nothing to do with passwords. The industry is bracing for the day a large, fault-tolerant quantum computer runs Shor’s algorithm against the public-key cryptography underneath everything: the headline math says a key that costs classical computers millennia falls in days. RSA is the poster victim; elliptic curves fall to the same algorithm, on timelines the literature still argues about. Whether that machine arrives in ten years or thirty is a debate I will not settle here. The readiness posture is settleable today.

The replacement algorithms exist; NIST finished standardizing the first wave in 2024. The friction is operational: post-quantum keys and signatures are an order of magnitude larger, TLS handshakes bloat, and proposals like Merkle-tree certificates are still being argued as the future shape of the public PKI. Nobody wants to forklift their certificate estate twice while that settles. So most estates wait, frozen, with ten-year roots and appliance certs that expire in 2031.

Short-lived identity changes the proportions of that problem. First, agility: in this demo the CA’s algorithm is one configuration line, ca_key_type = "ec-p256", and because every credential in the fleet reissues itself inside a rotation cycle, changing that line re-roots the whole estate in minutes, not in a two-year migration program. The industry has already rehearsed this motion once: when SSH moved from RSA to Ed25519, the fleets that rotated keys as a habit barely noticed, and the ones that had never rotated anything are still finding 1024-bit keys in authorized_keys files today. Second, the window: a “harvest now, decrypt later” adversary, recording traffic today to break it on a future machine, is harvesting, in this design, credentials that died fifteen minutes after issuance. Breaking a certificate after it expires buys a key to a door that no longer exists.

I am not selling rotation as the answer to quantum computing. It is not: recorded traffic still needs post-quantum key exchange, and signatures will need the new algorithms once the payload sizes are livable. But readiness, stripped of vendor theater, is mostly crypto-agility: the ability to change your cryptography faster than the threat changes. A fleet that reissues every credential every fifteen minutes has that ability as a built-in property. A fleet holding a password from 2019 does not.

Where this goes: non-human identity, Kubernetes, Cilium, and eBPF

Strip away the tooling and the shift is one sentence: stop authenticating what a process knows, start authenticating what a process verifiably is. Names, not secrets. Evidence, not strings. Expiry as the default state, so that everything stolen dies young.

Once every workload carries a verifiable name, other things snap into place. Authorization becomes “which names may talk” instead of “which IP ranges may connect.” And the place this stack stops being a retrofit and starts being beautiful is an orchestrated fleet on modern infrastructure. That is the next article in this series: the same SPIFFE attestation on Kubernetes with Cilium, where eBPF enforces the policy directly in the Linux kernel, on the namespaces and network paths themselves. What that buys is a network segregation layer that neither knows nor cares whether a workload runs on cloud, on-prem, a neocloud, or in which region. Named workloads, one policy, and the routing and granular-access complexity of a large footprint abstracted underneath. It just works, and it follows the workload, not the subnet.

The database will still not know any passwords.