External Secrets Operator and Bitwarden Secrets Manager¶
The documentation at https://external-secrets.io/latest/provider/bitwarden-secrets-manager/ is pretty dry and I made mistakes at first.
TL;DR
The SDK server ended up in cert-manager because I used the CA cert as a server
cert, which chained namespace dependencies together. The fix is what the docs
intended all along: issue a leaf cert in the SDK server's namespace, signed by
the CA ClusterIssuer. No extra tools (reflector, replicator) needed — just the
cert-manager primitives that were already there.
The Setup¶
If you use External Secrets Operator with Bitwarden Secrets Manager, you need the bitwarden-sdk-server — a thin REST wrapper around the Bitwarden Rust SDK (~150MB compiled). ESO talks to it over HTTPS (mandatory, no HTTP option), which means TLS certificates are involved.
The communication chain looks like this:
ExternalSecret → ESO Controller → HTTPS → bitwarden-sdk-server → Bitwarden Rust SDK → Bitwarden Cloud API
What I Did Wrong¶
The official example sets up a two-tier PKI:
- A self-signed CA certificate (
isCA: true) — the signing authority - A leaf certificate — the actual TLS cert the SDK server uses, signed by the CA
- A
ClusterIssuerreferencing the CA to sign the leaf
I misread the example and skipped step 2. I used the CA certificate directly as
the SDK server's TLS serving cert. This works — Go's TLS verification is happy
because the cert is self-signed and the caBundle in the SecretStore points to
itself. But it created an accidental namespace prison.
The Namespace Trap¶
Here's the constraint chain that locked the SDK server into cert-manager:
-
ClusterIssuer reads CA secrets only from the cert-manager namespace — this is documented cert-manager behavior. A
ClusterIssuerwithca.secretNamelooks for that secret in cert-manager's own namespace, not in the Certificate's namespace. -
The CA secret must live in cert-manager — because the ClusterIssuer needs it there.
-
The SDK server needs to mount that secret for TLS — because I was using the CA cert as the server cert.
-
Pods can't mount secrets from other namespaces — fundamental Kubernetes constraint.
-
Therefore the SDK server must be in cert-manager —
namespaceOverride: cert-managerin the Helm values.
Everything downstream followed: the SecretStore URL pointed to
*.cert-manager.svc.cluster.local, the certificate dnsNames matched that
namespace, and the hardcoded caBundle was baked to that cert.
What the Docs Actually Intended¶
The official example creates the CA in cert-manager but issues a separate
leaf certificate in whatever namespace the SDK server runs. The ClusterIssuer
can sign certificates in any namespace — that's the whole point of
ClusterIssuer vs Issuer. The resulting leaf cert secret lands in the target
namespace.
The SDK server doesn't care whether its TLS cert is a CA or a leaf. It just
calls tls.LoadX509KeyPair(certFile, keyFile). Any valid cert+key pair works.
I also mistakenly believed isCA: true was a requirement of the SDK server
itself. It's not — isCA: true is for the CA's role in the PKI chain. The
comment # this is discouraged but required by ios in the official example
refers to iOS Bitwarden clients, not the SDK server.
The Fix¶
Create a leaf certificate in external-secrets-system, signed by the existing
CA ClusterIssuer:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: bitwarden-sdk-server-tls
namespace: external-secrets-system
spec:
secretName: bitwarden-sdk-server-tls
duration: 2160h
renewBefore: 360h
dnsNames:
- bitwarden-sdk-server.external-secrets-system.svc.cluster.local
issuerRef:
name: bitwarden-certificate-issuer
kind: ClusterIssuer
cert-manager deposits the leaf cert secret in external-secrets-system. The SDK
server mounts it locally. No cross-namespace mounts, no namespaceOverride
hack.
The SecretStore's caBundle can reference the leaf secret's ca.crt key (
cert-manager includes the signing CA's public cert in the leaf secret), so it
auto-rotates too.
Remove the dnsNames and ipAddresses from the CA certificate — a CA doesn't need
them. Remove namespaceOverride: cert-manager from the ESO Helm values. Update
the SecretStore URL. Done.
Published: 2026-05-26