r/RedditEng • u/Okgaroo • 6d ago
Building Reddit Beyond Static Credentials: Reddit’s Journey to Workload Identity and mTLS
Written by Pratik Lotia
How we introduced automatically rotated workload credentials and migrated an existing service fleet to mutual TLS without a coordinated cutover.
One Service, Several Sets Of Credentials
Imagine building a new internal service. It calls another service, requires user context, reads from a database, requires access to a cloud resource and publishes events. Each dependency may have a different answer to the same question: how does the caller prove who it is and how do you manage access control?
With static credentials, the answer usually involves provisioning a secret, deciding where to store it, granting access to the consumers of the secret, setting up authorization rules and wiring it into the application. Repeat that for every dependency and for every Kubernetes cluster or cloud environment it deploys to. The service is eventually ready to run, but its credentials still need coordination across multiple teams making it a high friction and time consuming process. When a security control is cumbersome enough, your customers will often find a way to bypass it or adopt less practices which leads to other issues such leaking credentials in code, sharing tokens and lack of regular rotation.
We had accumulated versions of this problem across our infrastructure. Service authentication used static tokens. Database integrations had their own credential workflows. Services with an existing TLS requirement maintained separate certificate integrations. Depending on the system, credential onboarding could take weeks. There was also an operational problem where a service that received an unexpected burst of requests, couldn’t reliably identify the caller, let alone deny access to a caller.
The goal was a common identity system for workloads, with short-lived credentials issued and rotated automatically. We wanted the same foundation to work for service calls and infrastructure access, rather than asking each integration to introduce another secret.
Read on to find out how we succeeded in getting rid of static secrets for our modern database stack and getting 90% of our in-cluster Go grpc traffic to use authenticated calls in the form of mutual TLS.
Making The Work Worth Adopting
A project that changes how services authenticate needs support well beyond the security team. We had to make the case to leadership and partner teams that the investment would help them with product velocity. For product teams expanding an existing service, moving it to a new environment or launching a new one, use of a common identity system would significantly speed up product launches. Instead of designing another way to issue, distribute, and rotate client credentials, platform teams could build on the shared identity system and focus on deciding who should have access. This work stretched across multiple quarters and became one of the first real, sustained partnerships between Security, Platform and Product at Reddit. We chose mTLS to provide verified identities, confidentiality, and integrity through one shared integration, rather than asking teams to migrate again when they needed encrypted communication.
We started with teams that had needs like: a maintained TLS integration for sensitive services, fewer credential-provisioning steps, and a way to identify callers. Some early adopters already used TLS through a bespoke certificate path and wanted to move away from its scaling and maintenance constraints. For them, the value was replacing an existing operational dependency with something shared.
Performance was another reasonable concern. Modern TLS is fast but benchmarking your integrations for TLS connection establishment, certificate acquisition, latency and distinguishing those costs from steady-state request processing goes a long way in convincing product teams.
Adoption also needed an owner. Publishing a migration guide gave teams instructions, but it did not create room on their roadmaps. We eventually took on preparing migration changes ourselves, with service owners reviewing and validating the behavior they knew best. That decision substantially changed the amount of work we could complete.
Choosing SPIFFE And SPIRE
We had already invested in moving service communication to gRPC. Our identity model needed to fit into that foundation. We also wanted to avoid vendor lock-in. We chose SPIFFE as the identity standard and SPIRE as its implementation. SPIFFE defines how a workload identity is represented and how software obtains and verifies credentials for it. SPIRE performs the attestation and issuance work. The SPIFFE community’s book, Solving the Bottom Turtle, is a useful introduction to the problem and the design.
We considered introducing a service mesh that provides mTLS along with routing, retries, and other traffic controls. We already had investments in service discovery, transport, and application frameworks, and our immediate requirement was identity. Adopting SPIRE directly let us integrate with those systems without coupling the project to replacing the traffic platform or retrofitting a sidecar model across the fleet.
That choice left us responsible for the application integration. SPIRE would verify workloads and issue credentials while our libraries still had to retrieve them, handle rotation, configure TLS, and expose the peer identity to application code. Workloads receive a short-lived X.509 certificate carrying its SPIFFE ID which translates to the URI SAN, corresponding private-key material, and the trust bundle needed to verify peers. We use deployment and workload attributes to distinguish SPIFFE IDs at a pod level and policies can select the stable service attributes to define authorization.
Our deployment uses a nested hierarchy of issuers so workloads can share a trust foundation across k8s clusters while issuance is handled locally. We evaluated independent trust domains and federation as well. Federation provides useful isolation, but it also requires managing trust-bundle relationships as cluster environments change.
Cluster creation and node replacement are routine operations. We automated identity infrastructure setup as part of cluster provisioning, including the registration work needed for a new issuer to join the hierarchy. A security engineer should not have to manually arrange trust every time infrastructure creates a cluster.

Putting Identity Into The Service Framework
Most service owners should not need to know how to provision an intermediate CA or refresh an X.509 source. They need a supported way to start a server, connect to a dependency, and test the result.
We integrated identity into Baseplate, Reddit’s service framework, and our infrastructure-generation tooling. The infrastructure integration makes the SPIRE API available to the application. The library obtains credentials, configures the client or server, manages credential rotation without requiring a service restart and exposes authenticated peer information. Standard templates carry that setup into new services.
Lifecycle management matters just as much. During shutdown, a service may stop accepting new work while in-flight requests still make outbound calls. We encountered a case where the shared credential source could close before those calls finished. The fix was to keep it available through service draining and release it afterward. Authentication infrastructure had become part of the application lifecycle, so it needed the same care as a connection pool.
Developers also needed to exercise the integration in their normal development environment. We built on Snoodev, Reddit’s development environment, so changes could be tested with the same identity APIs used by the application. That gave migration reviews evidence beyond a successful CI compile.
The receiving service uses the verified identity for authorization. Authentication establishes who the peer is; policy determines whether that peer may connect or invoke a particular method. Keeping those responsibilities distinct helped us reason about both the API and the migration sequence.
Migrating Without A Coordinated Cutover
Our early adopters added this integration using a separate listener. A service could add a new TLS port while retaining its existing listener, then move clients over incrementally. That worked for the initial use cases. Expanding the pattern across the fleet exposed a mismatch with service discovery. Our discovery model did not represent separate plaintext and TLS destinations in the way a general dual-port rollout would require. We could change that model, or find a migration shape that kept existing addresses working. From experience, we knew that simplifying adoption will yield faster gains.
We chose a permissive listener for the general migration. It accepts a connection on the existing port, examines the initial bytes, and hands it to the appropriate TLS or plaintext path. Existing clients keep using the same address. TLS-capable clients can negotiate mTLS when the destination supports it. This let us deploy server and client support in the same migration change without requiring every upstream dependency to move first. It removed a large coordination dependency from the service graph. A migrated service could receive calls from older clients while beginning to use TLS with compatible dependencies. Our Kafka migration faced a similar constraint: change the infrastructure without requiring every client team to move together and we decided to continue a similar approach.
The compatibility path had a cost. A listener that supports TLS is not the same as one that requires it. Keeping the address and port unchanged also means port-level traffic counts cannot tell you whether a connection used TLS. We had to track the negotiated transport in application metrics and treat the eventual cutover as separate work.
Authorization needed a staged rollout too. Before this project, we did not have a complete, verified view of callers. Immediately rejecting every caller missing from a newly written policy would turn gaps in our dependency inventory into outages. Audit mode let owners compare proposed rules with observed calls before enforcement. Observing a caller is evidence that it exists, not approval that it should have access; owners still need to make that decision.
Metrics became part of the migration interface. A successful rollout meant more than a server starting: we checked credential acquisition, TLS setup, actual request traffic, and which peers were using the new path.
Large callers needed smaller batches. A service with many outbound dependencies could not be treated like a service with one client. Shared client libraries sometimes hid connection setup, and multiple logical clients could share an underlying connection. We separated library support from activation where needed, then enabled groups of dependencies and watched startup and request behavior.
Compatibility code also needed its own tests. Looking at the first bytes of a connection sounds trivial, but doing a blocking read in an accept loop can let one silent connection delay unrelated healthy clients. We fixed that behavior by separating acceptance from connection classification. The migration wrapper had to preserve the server’s availability properties as well as its protocol behavior.
Changing Who Did The Migration Work
The first approach was to find motivated adopters, help them migrate, and turn what we learned into documentation. It was useful for proving the integration, but it did not scale to every team independently finding time for another infrastructure project.
We changed the ownership model. The security team prepared the code and configuration changes, worked through validation, and brought service owners a pull request they could assess. That required understanding each service’s setup, not just changing a library version. AI-assisted coding helped with that repetitive discovery and editing. We encoded the migration process in a reusable set of instructions: inspect the service, identify clients and listeners, update infrastructure wiring, run tests, exercise the development environment, and prepare the pull request. The instructions also captured exceptions found during previous migrations.
We began with roughly five services per week. These were services we started working on, not necessarily changes merged and deployed that week. After building confidence in the process, we increased the pace to about twenty per week and expanded to higher-traffic services. AI-assisted coding, as some of you already have experienced, is not foolproof. It could update an obvious client and miss another one created through a shared library. Some libraries did not yet expose the TLS integration their consumers needed. Those cases required changes at the abstraction that owned the connection, followed by another pass over the consuming services.
We kept human review, testing, and deployment validation in the loop. Generating a plausible diff was useful, but it did not prove that the intended traffic was encrypted. The same telemetry used to track the rollout helped us find incomplete migrations and refine the instructions. Early in the rollout, TLS was often the first suspect when a migrated service had an incident. We rolled back changes where needed to restore service, then investigated; in most cases, the cause turned out to be elsewhere. As successful migrations accumulated, confidence grew and those suspicions became less frequent.
The result was a migration process that improved as it encountered more of the fleet. Automation reduced the cost of preparing changes; service knowledge and observed behavior still determined whether they were ready.
Beyond Service-To-Service Calls
A workload identity becomes more useful when other systems can consume it. A service that has already established its identity should not need to acquire another static credential merely to access a supported database or publish an event.
Different database-related backends required different approaches to support our identity model. Some systems understood URI SANs (our standard SPIFFE ID) directly. Others supported client certificates but expected a DNS subject alternative name or a subject common name (CN) when mapping a client to permissions. We added certificate composition support for those integrations, alongside extension support where additional verified attributes are needed.
Those fields were not interchangeable. A DNS SAN used by a particular database is a compatibility mechanism; it does not replace the SPIFFE identity or define a universal permission model. The issuer needs a trustworthy basis for any attribute it signs, and the receiving system still owns the authorization decision. We can share credential issuance without pretending every backend has the same access-control API.
Our TiDB integration is one production example. It supported clients to authenticate using workload credentials but we had to add upstream support to simplify mounting SPIFFE helpers and authorizing clients with regex support. It enabled us to remove the separate database credential provisioning step. Additionally, our integrations with event publishing and data infrastructure reuse the same identity foundation, while other database systems are at different stages of implementation.
Our internal LLM Platform is another early adopter. It verifies a backend workload’s identity and checks project permissions before allowing model access. A project identifier alone does not grant permission. Client services can use the platform without holding individual provider API keys, while the platform retains project-level access controls and usage attribution.
What Changed, And What Comes Next
Within four months of mass migrations, we migrated 95% of Go services by service count and 90% by in-cluster traffic coverage. For developers, the change is that supported integrations start with a workload identity and automatic credential rotation. Teams only need to decide what access a service should have, but they do not need to invent a credential lifecycle for every dependency. For operators, verified caller metrics provide a more useful starting point when investigating unexpected traffic.
We are continuing to reduce the attack surface by expanding language and backend support, extending protected communication across network boundaries, integrating SPIFFE identities with cloud-provider IAM roles and making authorization easier to apply consistently. Completing a migration also means verifying that the credential or transport it replaced is no longer needed, then retiring it. Adoption dashboards are useful, but they are not a substitute for that check.
That’s a wrap, if you enjoyed reading this article, I encourage you to check out some of my other articles:
Did you know we're actively hiring? If you made it this far then you're obviously interested in the types of problems we're solving. So why not solve them with us?


















































































