# Kubernetes TLS secrets

To create a Kubernetes TLS secret, run `kubectl create secret tls example-com-tls --cert=fullchain.crt --key=private.key`. The `--cert` file must contain the **full chain** — your certificate followed by its intermediates — because Ingress controllers serve whatever is in that file verbatim and do not fetch missing links. The secret type is `kubernetes.io/tls`, and the two keys inside are always named `tls.crt` and `tls.key` regardless of your filenames.

Source: https://certguru.io/platforms/kubernetes-tls-secret/
Last updated: 2026-08-19
Commands verified against OpenSSL 3.5.7.

---

## Step 1 — build the chain

```bash
cat certificate.crt intermediate.crt > fullchain.crt
```

> **Ingress serves exactly what you give it**
>
> There is no chain-completion step anywhere in the path. A `tls.crt` containing only the leaf
>   produces a site that works in browsers and fails for every API client — see
>   [unable to get local issuer certificate](/errors/unable-to-get-local-issuer-certificate/).

## Step 2 — create the secret

```bash
kubectl create secret tls example-com-tls \
  --cert=fullchain.crt \
  --key=private.key \
  --namespace=production
```

Idempotently, so it can live in a script:

```bash
kubectl create secret tls example-com-tls \
  --cert=fullchain.crt --key=private.key \
  --namespace=production \
  --dry-run=client -o yaml | kubectl apply -f -
```

## Step 3 — reference it from the Ingress

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-com
  namespace: production
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - example.com
        - www.example.com
      secretName: example-com-tls
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
```

## Writing the secret as YAML directly

The keys are always `tls.crt` and `tls.key`, whatever your files were called:

*Base64 the inputs*

```bash
base64 -w0 fullchain.crt
base64 -w0 private.key
```

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: example-com-tls
  namespace: production
type: kubernetes.io/tls
data:
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...
  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0t...
```

> **Do not commit this file**
>
> `data` is base64, which is encoding, not encryption. A secret manifest in Git is a private key in
>   Git. Use Sealed Secrets, External Secrets, SOPS, or cert-manager instead.

## Inspecting an existing secret

*Read the certificate out of a secret*

```bash
kubectl get secret example-com-tls -n production \
  -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -dates -ext subjectAltName
```

*How many certificates are in the chain?*

```bash
kubectl get secret example-com-tls -n production \
  -o jsonpath='{.data.tls\.crt}' | base64 -d | grep -c "BEGIN CERTIFICATE"
```

Expected output:

```
2
```

A count of 1 means the chain is incomplete.

## Checking the key matches before applying

Worth doing in CI. A mismatched pair produces an Ingress that fails to serve and an error message
that does not point here:

```bash
[ "$(openssl pkey -in private.key -pubout | openssl sha256)" \
  = "$(openssl x509 -in fullchain.crt -noout -pubkey | openssl sha256)" ] \
  || { echo "key/cert mismatch"; exit 1; }
```

## cert-manager

For anything ongoing, cert-manager handles issuance and renewal and writes the secret for you:

```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
  namespace: production
spec:
  secretName: example-com-tls
  dnsNames:
    - example.com
    - www.example.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
```

The resulting secret has the same shape, so the Ingress above needs no changes.


## Frequently asked questions

### Does the secret need the full chain?

Yes. Ingress controllers serve the contents of tls.crt exactly as given. If it holds only the leaf, clients cannot build a path to a trusted root and verification fails outside browsers.

### Are the contents encrypted?

No. Kubernetes Secrets are base64-encoded, not encrypted, and are readable by anyone with get access to the namespace. Enable encryption at rest on etcd and use RBAC to restrict access.

### Must the secret be in the same namespace as the Ingress?

Yes. An Ingress can only reference secrets in its own namespace. A wildcard certificate used across namespaces has to be copied into each, or replicated by a controller such as reflector.

### How do I update a certificate without downtime?

Apply the new secret with the same name — `kubectl create secret tls ... --dry-run=client -o yaml | kubectl apply -f -`. Most Ingress controllers watch the secret and reload without dropping connections.


## References

- [Kubernetes secrets](https://kubernetes.io/docs/concepts/configuration/secret/) — the kubernetes.io/tls type and its required keys

## Related pages

- https://certguru.io/concepts/certificate-chain/
- https://certguru.io/errors/unable-to-get-local-issuer-certificate/
- https://certguru.io/platforms/nginx/
- https://certguru.io/openssl/check-key-matches-certificate/
- https://certguru.io/convert/pfx-to-pem/
- https://certguru.io/errors/x509-signed-by-unknown-authority-go/

--
CertGuru — https://certguru.io
