## Intro

Kubernetes Custom Resource Definitions (CRDs) extend the Kubernetes API, but versioning introduces security risks that are often overlooked. A CRD may have multiple versions (e.g., v1alpha1, v1beta1, v1), and each version may have a different schema, validation rules, and conversion logic. If not properly secured, versioning can lead to privilege escalation, data corruption, or unintended access. This guide provides a practical approach to hardening CRD versioning, with concrete commands, configuration examples, and verification steps.

The target audience includes platform engineers, DevOps consultants, and technical startup teams responsible for maintaining Kubernetes clusters. The focus is on operational safety: observe before changing, minimize blast radius, use placeholders instead of secrets, verify every step, and document recovery procedures.

Throughout this guide, we use a sample CRD called widgets.example.com to illustrate concepts. We assume a cluster running Kubernetes 1.25+ with kubectl configured and appropriate permissions to create CRDs and access the API server.

## Version and Environment Inventory

Before making any changes, inventory the current state of your CRDs and their versions. This includes identifying installed CRDs, their versions, storage status, and conversion strategies.

Start by listing all CRDs in the cluster:

kubectl get crd 
 Expected output includes a list of CRD names, their created dates. To see details for a specific CRD, use:

kubectl get crd widgets.example.com -o yaml 
 This outputs the full specification. Look for the spec.versions field to see all versions defined. For example:

spec:
 versions:
 - name: v1beta1
 served: true
 storage: false
 schema: ...
 - name: v1
 served: true
 storage: true
 schema: ... 
 Pay attention to:

- Which version is the storage version (only one can be storage).

- Which versions are served (clients can use them).

- Whether a conversion strategy is defined (e.g., None or Webhook ).

Check if any conversion webhooks are configured:

kubectl get crd widgets.example.com -o jsonpath='{.spec.conversion}' 
 If the output includes strategy: Webhook , then conversion is handled by a webhook. Note the webhook's service and path, as this is a potential security risk if not properly authenticated.

Also inspect existing custom resources of that kind to see which versions are in use:

kubectl get widgets.example.com --all-namespaces -o wide 
 The -o wide may include the version in the VERSION column. If not, use -o custom-columns to display it:

kubectl get widgets.example.com --all-namespaces -o custom-columns=NAME:.metadata.name,NAMESPACE:.metadata.namespace,APIVERSION:.apiVersion 
 Record the current state and timestamps. This inventory helps you understand the impact of any versioning changes. Keep a copy of the CRD definition and a list of resources before making modifications.

Practical environment inventory checklist:

- kubectl version to confirm client and server versions.

- kubectl get crd widgets.example.com -o yaml > crd-backup.yaml to back up the current definition.

- kubectl get widgets.example.com -A -o json > widgets-backup.json to back up all instances.

- kubectl get apiservice | grep widgets.example.com to see the aggregated API service status.

- kubectl describe crd widgets.example.com to see events and status conditions.

<style>
.eno-quiz-widget{margin:2rem 0;padding:1.5rem;border-radius:12px;background:var(--eno-surface-lowest,#f7f7f8);border:1px solid var(--eno-border-soft,#e2e2e6);font-family:var(--eno-font-body,Inter,sans-serif)}
.eno-quiz-widget .eno-quiz-kicker{font-family:var(--eno-font-label,"JetBrains Mono",monospace);font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--eno-text-muted,#5f5f68);margin:0 0 .5rem}
.eno-quiz-widget .eno-quiz-question{font-family:var(--eno-font-heading,"Hanken Grotesk",sans-serif);font-size:1.0625rem;font-weight:600;margin:0 0 1rem;color:var(--eno-text-strong,#1a1a1f)}
.eno-quiz-widget .eno-quiz-options{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.eno-quiz-widget .eno-quiz-option{display:block;width:100%;min-height:44px;text-align:left;padding:.625rem .875rem;border-radius:8px;border:1.5px solid var(--eno-border-soft,#e2e2e6);background:#fff;font-size:.9375rem;cursor:pointer;transition:border-color var(--eno-motion-base,180ms ease-out),background var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-option:hover{border-color:var(--eno-primary,#0059bb)}
.eno-quiz-widget .eno-quiz-option:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-option[aria-pressed="true"]{border-color:var(--eno-primary,#0059bb);background:rgb(0 89 187 / 0.06)}
.eno-quiz-widget .eno-quiz-option[data-correct="true"].eno-quiz-revealed{border-color:var(--eno-success,#17803a);background:rgb(23 128 58 / 0.08)}
.eno-quiz-widget .eno-quiz-option[data-correct="false"].eno-quiz-revealed.eno-quiz-was-selected{border-color:var(--eno-error,#ba1a1a);background:var(--eno-error-soft,#ffdad6)}
.eno-quiz-widget .eno-quiz-option-icon{display:inline-block;width:1.1em;margin-right:.4em;font-weight:700}
.eno-quiz-widget .eno-quiz-submit{margin-top:1rem;min-height:44px;padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--eno-primary,#0059bb);color:#fff;font-weight:600;font-size:.9375rem;cursor:pointer;transition:opacity var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-submit:disabled{opacity:.5;cursor:not-allowed}
.eno-quiz-widget .eno-quiz-submit:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-explanation{margin-top:1rem;padding:.875rem 1rem;border-radius:8px;font-size:.9375rem;line-height:1.5;display:none}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-visible{display:block}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-correct{background:rgb(23 128 58 / 0.08);color:var(--eno-success,#17803a)}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-incorrect{background:var(--eno-error-soft,#ffdad6);color:var(--eno-error,#ba1a1a)}
@media (prefers-reduced-motion: reduce){.eno-quiz-widget *{transition:none!important}}
</style><div class="eno-quiz-widget" role="group" aria-label="Quick check question">
Quick check 1 of 2

What is the only version that can be marked as storage in a CRD?

- The oldest version
- The newest version
- One and only one version
- All versions can be storage

<button type="button" class="eno-quiz-submit" disabled>Submit</button>
<div class="eno-quiz-explanation">According to the CRD example, a CRD can have multiple versions, but one and only one version must be marked as the storage version.</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

## Safe Configuration Path

When configuring CRD versioning security, follow the principle of least privilege and validate everything. The safe path involves defining proper schemas, RBAC, and using conversion webhooks securely.

### 1. Define Strict Schemas

Each version of your CRD should have a schema that validates the structure and constraints. Use OpenAPI v3 schema validation. A weak schema can allow arbitrary fields, which might be exploited. Example for v1 :

schema:
 openAPIV3Schema:
 type: object
 required: ["spec"]
 properties:
 spec:
 type: object
 required: ["size"]
 properties:
 size:
 type: integer
 minimum: 1
 maximum: 100
 replicas:
 type: integer
 minimum: 0
 maximum: 10
 additionalProperties: false
 additionalProperties: false 
 Set additionalProperties: false to reject unknown fields. This prevents attackers from injecting unexpected data.

### 2. Implement RBAC for CRD Versions

RBAC can control access to specific API versions. By default, permissions granted on a resource apply to all versions. To restrict access to certain versions, you can use resourceNames or separate roles. For example, to allow only reading v1 resources but not v1beta1 , create a role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
 namespace: default
 name: widget-v1-reader
rules:
- apiGroups: ["example.com"]
 resources: ["widgets"]
 verbs: ["get", "list", "watch"]
 # No way to specify version directly, but you can use resourceNames or rely on aggregation. 
 Actually, Kubernetes RBAC does not support version-specific authorization directly. The API group is specified, but not the version. To enforce version restrictions, you need to use admission webhooks or separate API groups per version. Alternatively, you can use OPA/Gatekeeper policies. We'll cover admission webhooks later.

### 3. Use Conversion Webhooks Securely

If you have multiple versions, conversion webhooks allow Kubernetes to convert resources between versions. The webhook must be served over HTTPS with a valid certificate. Configure the CRD conversion with caBundle to trust the webhook's CA. Example:

conversion:
 strategy: Webhook
 webhook:
 conversionReviewVersions: ["v1", "v1beta1"]
 clientConfig:
 service:
 namespace: conversion-webhook
 name: conversion-service
 path: /convert
 caBundle: <base64-encoded CA certificate> 
 Always set conversionReviewVersions to the versions your webhook supports. Ensure the webhook service is only accessible within the cluster and uses mutual TLS if possible.

### 4. Apply Changes Gradually

When modifying CRD versioning, avoid removing or changing served versions abruptly. Follow a gradual process:

- First, add a new version with served: true and storage: false .

- Allow clients to migrate, then set storage: true after testing.

- Deprecate the old version by setting served: false later.

- Finally, remove the old version if no resources use it.

To check which resources are still using an old version:

kubectl get widgets.example.com --all-namespaces --field-selector apiVersion=example.com/v1beta1 
 Or use:

kubectl get widgets.example.com --all-namespaces -o json | jq '.items[] | select(.apiVersion=="example.com/v1beta1")' 

### Example: Safe CRD Update

 Assume you want to add v2 as the new storage version. Steps:

- Backup current CRD:

kubectl get crd widgets.example.com -o yaml > crd-backup.yaml 

- Edit CRD to add v2 with served: true and storage: false :

 kubectl edit crd widgets.example.com 
 Add under spec.versions :

- name: v2
 served: true
 storage: false
 schema: ... 
 Save and exit.

- Verify v2 is served but not storage:

kubectl get crd widgets.example.com -o jsonpath='{.spec.versions[*].name} {"\n"}'
kubectl get crd widgets.example.com -o jsonpath='{.spec.versions[?(@.storage==true)].name}' 
 Expected first output lists all versions including v2, second output still shows v1.

- Test creating a v2 resource:

kubectl apply -f - <<EOF
apiVersion: example.com/v2
kind: Widget
metadata:
 name: test-widget-v2
spec:
 size: 10
EOF 

- After successful test, switch storage to v2:

 kubectl patch crd widgets.example.com --type='json' -p='[{"op": "replace", "path": "/spec/versions/1/storage", "value": true}, {"op": "replace", "path": "/spec/versions/0/storage", "value": false}]' 
 (Assuming versions array order: index 0 = v1, index 1 = v2). Adjust paths accordingly.

- Verify storage version changed:

kubectl get crd widgets.example.com -o jsonpath='{.spec.versions[?(@.storage==true)].name}' 
 Output: v2 .

## Verification and Diagnostics

After making changes, verify that everything works as expected and diagnose any issues.

### Check CRD Status

Use kubectl describe crd to see status conditions:

kubectl describe crd widgets.example.com 
 Look for Established condition true and no NonStructuralSchema warnings. If the schema is not structural, Kubernetes may reject the CRD or fail to serve it.

### Test Resource Operations

Attempt to create, get, and list resources in each version:

# Create v1 resource
kubectl apply -f - <<EOF
apiVersion: example.com/v1
kind: Widget
metadata:
 name: test-v1
spec:
 size: 5
EOF

# Get via v1
kubectl get widgets.v1.example.com test-v1 -o yaml

# Get via v2 (conversion should happen if webhook configured)
kubectl get widgets.v2.example.com test-v1 -o yaml 
 If conversion is not set, the v2 get will fail because storage version is v2 but the object was created with v1? Actually, if storage is v2 and you create a v1 resource, Kubernetes will convert it to v2 for storage if a conversion webhook exists, or if no conversion, it will reject. Verify expected behavior.

### Check Audit Logs

Enable Kubernetes audit logging to monitor access to CRD versions. Look for requests to /apis/example.com/v1beta1/widgets if you are trying to detect usage of deprecated versions. Example audit policy snippet:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
 resources:
 - group: "example.com"
 resources: ["widgets"] 
 Then check audit logs with tools like kubectl logs -n kube-system kube-apiserver-<node> or if using managed Kubernetes, use cloud provider logging.

### Diagnose Conversion Webhook Failures

If conversion webhook is misconfigured, you may see errors like:

Check webhook pod logs:

- conversion webhook not reachable

- x509: certificate signed by unknown authority

kubectl logs -n conversion-webhook deploy/conversion-service 
 Test webhook endpoint directly from within cluster:

kubectl run -it --rm debug --image=curlimages/curl -- sh
# inside pod:
curl -k https://conversion-service.conversion-webhook.svc/convert -d '{"apiVersion":"apiextensions.k8s.io/v1","kind":"ConversionReview",...}' 
 Ensure caBundle is correctly base64-encoded. Use base64 -w0 on Linux.

<style>
.eno-quiz-widget{margin:2rem 0;padding:1.5rem;border-radius:12px;background:var(--eno-surface-lowest,#f7f7f8);border:1px solid var(--eno-border-soft,#e2e2e6);font-family:var(--eno-font-body,Inter,sans-serif)}
.eno-quiz-widget .eno-quiz-kicker{font-family:var(--eno-font-label,"JetBrains Mono",monospace);font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--eno-text-muted,#5f5f68);margin:0 0 .5rem}
.eno-quiz-widget .eno-quiz-question{font-family:var(--eno-font-heading,"Hanken Grotesk",sans-serif);font-size:1.0625rem;font-weight:600;margin:0 0 1rem;color:var(--eno-text-strong,#1a1a1f)}
.eno-quiz-widget .eno-quiz-options{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.5rem}
.eno-quiz-widget .eno-quiz-option{display:block;width:100%;min-height:44px;text-align:left;padding:.625rem .875rem;border-radius:8px;border:1.5px solid var(--eno-border-soft,#e2e2e6);background:#fff;font-size:.9375rem;cursor:pointer;transition:border-color var(--eno-motion-base,180ms ease-out),background var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-option:hover{border-color:var(--eno-primary,#0059bb)}
.eno-quiz-widget .eno-quiz-option:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-option[aria-pressed="true"]{border-color:var(--eno-primary,#0059bb);background:rgb(0 89 187 / 0.06)}
.eno-quiz-widget .eno-quiz-option[data-correct="true"].eno-quiz-revealed{border-color:var(--eno-success,#17803a);background:rgb(23 128 58 / 0.08)}
.eno-quiz-widget .eno-quiz-option[data-correct="false"].eno-quiz-revealed.eno-quiz-was-selected{border-color:var(--eno-error,#ba1a1a);background:var(--eno-error-soft,#ffdad6)}
.eno-quiz-widget .eno-quiz-option-icon{display:inline-block;width:1.1em;margin-right:.4em;font-weight:700}
.eno-quiz-widget .eno-quiz-submit{margin-top:1rem;min-height:44px;padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--eno-primary,#0059bb);color:#fff;font-weight:600;font-size:.9375rem;cursor:pointer;transition:opacity var(--eno-motion-base,180ms ease-out)}
.eno-quiz-widget .eno-quiz-submit:disabled{opacity:.5;cursor:not-allowed}
.eno-quiz-widget .eno-quiz-submit:focus-visible{outline:none;box-shadow:0 0 0 3px rgb(0 89 187 / 0.15)}
.eno-quiz-widget .eno-quiz-explanation{margin-top:1rem;padding:.875rem 1rem;border-radius:8px;font-size:.9375rem;line-height:1.5;display:none}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-visible{display:block}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-correct{background:rgb(23 128 58 / 0.08);color:var(--eno-success,#17803a)}
.eno-quiz-widget .eno-quiz-explanation.eno-quiz-incorrect{background:var(--eno-error-soft,#ffdad6);color:var(--eno-error,#ba1a1a)}
@media (prefers-reduced-motion: reduce){.eno-quiz-widget *{transition:none!important}}
</style><div class="eno-quiz-widget" role="group" aria-label="Quick check question">
Quick check 2 of 2

When adding v2 as a new served version but not storage, what should you set for v2's `served` and `storage` flags?

- served: true, storage: true
- served: true, storage: false
- served: false, storage: true
- served: false, storage: false

<button type="button" class="eno-quiz-submit" disabled>Submit</button>
<div class="eno-quiz-explanation">The guide&#39;s safe CRD update example shows adding v2 with served: true and storage: false to test before switching storage.</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('.eno-quiz-widget:not([data-eno-quiz-bound])').forEach(function (widget) {
    widget.setAttribute('data-eno-quiz-bound', '1');
    var options = Array.prototype.slice.call(widget.querySelectorAll('.eno-quiz-option'));
    var submitBtn = widget.querySelector('.eno-quiz-submit');
    var explanation = widget.querySelector('.eno-quiz-explanation');
    var selected = null;
    options.forEach(function (opt) {
      opt.addEventListener('click', function () {
        if (widget.hasAttribute('data-eno-quiz-answered')) return;
        options.forEach(function (o) { o.setAttribute('aria-pressed', 'false'); });
        opt.setAttribute('aria-pressed', 'true');
        selected = opt;
        submitBtn.disabled = false;
      });
    });
    submitBtn.addEventListener('click', function () {
      if (!selected || widget.hasAttribute('data-eno-quiz-answered')) return;
      widget.setAttribute('data-eno-quiz-answered', '1');
      submitBtn.disabled = true;
      var correct = selected.getAttribute('data-correct') === 'true';
      options.forEach(function (o) {
        o.classList.add('eno-quiz-revealed');
        if (o === selected) o.classList.add('eno-quiz-was-selected');
        var icon = o.querySelector('.eno-quiz-option-icon');
        if (o.getAttribute('data-correct') === 'true') icon.textContent = '\u2713';
        else if (o === selected) icon.textContent = '\u2717';
      });
      explanation.classList.add('eno-quiz-visible', correct ? 'eno-quiz-correct' : 'eno-quiz-incorrect');
    });
  });
});
</script>

## Failure Modes and Recovery

Understand common failure modes and how to recover.

### Failure: CRD with invalid schema causes API server errors

If you apply a CRD with a non-structural schema or invalid OpenAPI, the API server may reject it. Example error:

The CustomResourceDefinition "widgets.example.com" is invalid: spec.validation.openAPIV3Schema.properties[spec].type: Required value: must not be empty at root 
 Recovery: correct the schema and reapply. If the CRD was previously working and you made a mistake, restore from backup:

kubectl apply -f crd-backup.yaml 

### Failure: Removing a version still in use

 If you set served: false for a version that has resources, those resources become inaccessible via the API, though they remain in etcd. You must migrate them before removing serving. To recover, set served: true again and perform migration.

### Failure: Conversion webhook down

If the conversion webhook is down, requests that require conversion will fail. Example error: conversion webhook not reachable . Recovery: fix the webhook deployment. If webhook cannot be restored quickly, you can change conversion strategy to None temporarily, but that may cause data inconsistency if multiple versions exist. Better to restore webhook.

### Failure: Storage version change causes data loss

Changing storage version without proper conversion can lead to data loss if fields are dropped. Always ensure conversion webhook is in place and tested before switching storage. If data loss occurs, restore from etcd backup if available.

### Recovery Steps

- Identify the failing operation and error messages.

- Check cluster events: kubectl get events --sort-by='.lastTimestamp' .

- Check API server logs for detailed errors (requires access to control plane).

- Backup current state: kubectl get crd widgets.example.com -o yaml > crd-current.yaml .

- Attempt to revert to previous known good CRD: kubectl apply -f crd-backup.yaml .

- If resources are missing or corrupted, restore from etcd snapshot (see etcd disaster recovery).

- After recovery, verify with read-only queries before enabling writes.

## Operations Checklist

Use this checklist before and after making versioning changes.

<div class="my-stack-md overflow-x-auto">
<table class="min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">#</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Check</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Command / Action</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Expected Result</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">1</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Backup CRD</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">kubectl get crd widgets.example.com -o yaml &gt; crd-backup-$(date +%Y%m%d).yaml</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">File created with current CRD definition</td></tr></tbody>
</table>
</div>
<div style="overflow-x:auto;margin:1.5rem 0;"><table style="width:100%;border-collapse:collapse;font-size:0.95rem;line-height:1.5;"><tbody><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">2</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">List existing resources by version</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">kubectl get widgets.example.com -A -o json | jq -r '.items[] | .apiVersion' | sort | uniq -c</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Count of resources per version</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">3</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Check conversion strategy</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">kubectl get crd widgets.example.com -o jsonpath='{.spec.conversion.strategy}'</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">None or Webhook</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">4</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Validate schema offline</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Use kubectl apply --dry-run=client -f crd-new.yaml</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">No schema errors</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">5</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Test on staging cluster first</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Apply changes to a non-production cluster</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">No unexpected behavior</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">6</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Monitor audit logs</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Tail audit logs for example.com group</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Only expected requests</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">7</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Verify each version's serving</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">kubectl get crd widgets.example.com -o jsonpath='{range .spec.versions[*]}{.name}{" served="}{.served}{" storage="}{.storage}{"\n"}{end}'</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Correct served/storage flags</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">8</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Test resource CRUD in all served versions</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Script that creates, gets, lists, deletes a test resource in each version</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Success without errors</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">9</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Check for deprecated version usage</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">kubectl get widgets.example.com -A -o json | jq '.items[] | select(.apiVersion | contains("v1beta1"))'</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Empty list (no resources using deprecated version)</td></tr><tr><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">10</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Document rollback procedure</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Write runbook for reverting CRD changes</td><td style="border:1px solid rgba(15,23,42,.14);padding:.75rem;vertical-align:top;">Clear steps for recovery</td></tr></tbody></table></div>

## Conclusion

Securing Kubernetes CRD versioning is essential for maintaining a robust and safe API extension system. By following the practices in this guide—inventorying versions, defining strict schemas, controlling access, using conversion webhooks securely, and verifying changes—you can prevent many common security and operational issues.

Start with a low-risk verification: inventory your current CRDs, identify their versions and conversion settings, and run the commands in the checklist. Then, gradually implement improvements, testing each change in a staging environment. Remember to always back up before making changes and have a rollback plan.

A reliable workflow makes failures visible, protects sensitive data, limits changes to intended resources, and defines recovery before an incident forces the decision. Apply these principles to harden your Kubernetes CRD versioning today.