# Kubernetes

The Kubernetes collector reads the certificates
[cert-manager](https://cert-manager.io/) manages in your clusters, so workload
certificates — the ones issued and renewed automatically, without anyone
requesting them — count toward your post-quantum readiness reporting.

It is **read-only**, and it **never reads Kubernetes Secrets**, so the private
keys your certificates reference are out of reach. That is a structural
property of the connector, not a setting you need to get right.

## What it collects

| Resource | What Mesh takes from it |
|---|---|
| `Certificate` (cert-manager) | The certificate inventory — key algorithm, key size, validity and renewal settings |
| `Issuer` / `ClusterIssuer` (cert-manager) | What signed each certificate. Resolved by name for every certificate; listing them in full is optional |
| `Pod` | Container image references, so a certificate can be tied to the workload using it |
| `ReplicaSet` (`apps`) | One owner hop, `Pod → ReplicaSet → Deployment`, so an image is attributed to a stable workload name instead of a pod hash that changes on every rollout |
| `Namespace` (`kube-system`) | Cluster identity, so two clusters are not confused for one another. Optional — if it is not granted, collection continues |

> **note**

Beyond certificates and issuers, the collector reads **Pod** container image
references and resolves the workload that owns them. Worth knowing before you
approve a cluster-wide credential — it reads what is *running*, not only what
is *issued*.

Certificates removed from a cluster are retired in Mesh after the next complete
collection.

## Before you start

- **cert-manager installed** in the cluster. If its custom resources are not
  registered, Mesh reports that it found none, rather than failing.
- **An API server URL reachable from Mesh**, over HTTPS.
- **A read-only credential** — a service account token, or a client
  certificate and key. Creating one is covered below.

## Decide your scope first

One decision shapes everything else: **which namespaces** you want inventoried.
It determines the Kubernetes permission you create, so it is worth settling
before you start.

| You want | Leave **Namespaces** | You grant |
|---|---|---|
| The whole cluster | Empty | A **ClusterRole** and `ClusterRoleBinding` |
| Only certain namespaces | A comma-separated list | A **Role** and `RoleBinding` in each one |

> **caution**

Leaving **Namespaces** empty makes one cluster-wide request per resource kind.
The Kubernetes API server does **not** quietly narrow that to the namespaces
your credential happens to cover — it refuses the whole request with `403`, and
the collection fails.

So if you grant per-namespace Roles, you **must** also list those namespaces in
the **Namespaces** field. Granting narrowly and leaving the field empty is the
most common way this connector fails its first run.

## Create the read-only access

First create the service account Mesh will authenticate as. This is the same
for either scope.

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: axiad-mesh-crypto
  namespace: kube-system
```

Then apply **one** of the following, matching the scope you chose above.

### Whole cluster

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: axiad-mesh-crypto-reader
rules:
  - apiGroups: ["cert-manager.io"]
    resources: ["certificates", "issuers", "clusterissuers"]
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
  - apiGroups: ["apps"]
    resources: ["replicasets"]
    verbs: ["get"]
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["get"]
  - apiGroups: ["apiextensions.k8s.io"]
    resources: ["customresourcedefinitions"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: axiad-mesh-crypto-reader
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: axiad-mesh-crypto-reader
subjects:
  - kind: ServiceAccount
    name: axiad-mesh-crypto
    namespace: kube-system
```

### Selected namespaces

Apply a `Role` and `RoleBinding` in **each** namespace you want inventoried,
and list those same namespaces in the **Namespaces** field.

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: axiad-mesh-crypto-reader
  namespace: my-namespace
rules:
  - apiGroups: ["cert-manager.io"]
    resources: ["certificates", "issuers"]
    verbs: ["get", "list"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
  - apiGroups: ["apps"]
    resources: ["replicasets"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: axiad-mesh-crypto-reader
  namespace: my-namespace
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: axiad-mesh-crypto-reader
subjects:
  - kind: ServiceAccount
    name: axiad-mesh-crypto
    namespace: kube-system
```

`ClusterIssuer` is cluster-scoped, so it cannot be granted by a namespaced
Role. If you are scoping to namespaces, leave **Enable Cluster Issuers** off.

### Get a token for the service account

```bash
kubectl -n kube-system create token axiad-mesh-crypto --duration=8760h
```

> **caution**

The API server may issue a token that expires **sooner** than you asked for —
many clusters cap bound-token lifetimes well below a year. Do not work out the
renewal date from the flag you typed.

Read the real expiry from the token's own `exp` claim, and set your reminder
from that. Decode it locally — the token is a live credential, so do not paste
it into an online decoder:

```bash
TOKEN=$(kubectl -n kube-system create token axiad-mesh-crypto --duration=8760h)
echo "$TOKEN" | python3 -c '
payload = sys.stdin.read().strip().split(".")[1]
payload += "=" * (-len(payload) % 4)
exp = json.loads(base64.urlsafe_b64decode(payload))["exp"]
print(datetime.datetime.fromtimestamp(exp, datetime.timezone.utc))'
```

An expired token stops collection, and the connector cannot warn you in
advance — so replace it before that date.

## Add the connector in Mesh

1. Go to **Settings › Connectors**, then select **Add connector**.
2. Under **Crypto collectors**, choose **Kubernetes**.
3. Fill in the fields below.
4. Select **Test configuration**, then **Add & connect**.

### Connection fields

| Field | Required | What to enter |
|---|---|---|
| **API Server URL** | Yes | The cluster's API server, for example `https://host:6443`. Must be `https://` — the credential travels with every request, so plain HTTP is refused |
| **Bearer Token** | One of | A service account token. Use **either** this **or** the client certificate and key — not both |
| **Client Certificate (PEM)** | One of | Sign in with a client certificate instead of a token. Supply the key below as well |
| **Client Key (PEM)** | With cert | The private key for that client certificate |
| **CA Certificate (PEM)** | No | The cluster's CA, so Mesh can verify it. Leave empty if the API server presents a certificate that is already publicly trusted |
| **Skip TLS Verification** | No | Default off. Lab use only — never enable it against a production cluster |
| **Namespaces** | No | Comma-separated. **Empty means cluster-wide** — see [Decide your scope first](#decide-your-scope-first) |
| **Enable Issuers** | No | Default off. Also list namespaced `Issuer` resources |
| **Enable Cluster Issuers** | No | Default off. Also list cluster-scoped `ClusterIssuer` resources |
| **cert-manager API Group** | No | Default `cert-manager.io`. Change only for a forked or pre-GA cert-manager |
| **cert-manager API Version** | No | Default `v1`. Change only for a pre-GA cert-manager |
| **Collection frequency** | No | How often Mesh re-reads the cluster, as a duration such as `1h` |

Paste credentials as **text**, not file paths — Mesh has no access to your
cluster's filesystem. The token and client key are write-only: once saved, they
are never shown again.

> **tip**

**Enable Issuers** and **Enable Cluster Issuers** control whether Mesh lists
issuers in full. The issuer behind each certificate is resolved by name
regardless, so leaving both off does not cost you the issuer of a certificate
you collected.

## Check the first collection

After **Add & connect**, open
[Data source syncs](../../../admin-guide/data-source-syncs/index.md) and find
the connector's first run. A healthy first run reports success for each part it
was granted.

Then look for the certificates themselves in your crypto asset inventory, and
in the **PQC Readiness Index** widget on your dashboard. A cluster with
cert-manager installed but no certificates issued yet reports a successful run
that collected nothing — that is the expected result, not a failure.

## Troubleshooting

| What you see | What it means |
|---|---|
| Run fails with a permission error, and you granted per-namespace Roles | **Namespaces** was left empty, so Mesh made a cluster-wide request. List your namespaces in that field, or grant a ClusterRole |
| One namespace missing, the rest fine | The credential has no Role in that namespace. Mesh records it as inaccessible and continues with the others |
| Certificates collected, but not attributed to a workload | `get` on `replicasets` was not granted. Images are still collected; only the owning Deployment is unresolved |
| "No certificates found", but you know they exist | cert-manager's custom resources are not registered in this cluster, or they are registered under a non-default API group or version. Check **cert-manager API Group** and **API Version** |
| Collection worked, then stopped | The token expired. Issue a new one and update the connector |
| TLS or certificate-verification errors | The API server presents a certificate Mesh does not trust. Paste the cluster CA into **CA Certificate (PEM)**. Do not reach for **Skip TLS Verification** against production |

If a run still fails after these, see
[Troubleshooting & support](../../../admin-guide/troubleshooting-and-support.md).
