Skip to content

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:

  1. A self-signed CA certificate (isCA: true) — the signing authority
  2. A leaf certificate — the actual TLS cert the SDK server uses, signed by the CA
  3. A ClusterIssuer referencing 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:

  1. ClusterIssuer reads CA secrets only from the cert-manager namespace — this is documented cert-manager behavior. A ClusterIssuer with ca.secretName looks for that secret in cert-manager's own namespace, not in the Certificate's namespace.

  2. The CA secret must live in cert-manager — because the ClusterIssuer needs it there.

  3. The SDK server needs to mount that secret for TLS — because I was using the CA cert as the server cert.

  4. Pods can't mount secrets from other namespaces — fundamental Kubernetes constraint.

  5. Therefore the SDK server must be in cert-managernamespaceOverride: cert-manager in 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