Deploy Kyverno on a RamNode VPS
A complete single-node Kyverno 1.18 deployment on k3s — Pod Security Standards baseline, custom policies, Policy Reporter UI, and a tested webhook-lockout recovery path.
At a Glance
| Project | Kyverno 1.18.0 (Helm chart 3.7.0) |
| Distribution | k3s v1.32.4+k3s1 (single node) |
| License | Apache 2.0 |
| Recommended Plan | 2 vCPU / 4 GB / 40 GB NVMe |
| OS | Ubuntu 24.04 LTS / Debian 12 |
| Estimated Setup Time | 45–60 minutes |
Why Kyverno on a Budget VPS
- Image registry enforcement — block
docker.ioif you only allow private/ghcr.io - Required labels and annotations (
app,owner,cost-center) - Auto-generate
NetworkPolicy,LimitRange,ResourceQuotaper namespace - Cluster-wide Pod Security Standards without writing admission controllers
- Mutation — inject sidecars, default resource requests, or
imagePullSecretsautomatically
Prepare the VPS
apt update && apt upgrade -y
apt install -y curl wget gnupg ca-certificates apt-transport-https
hostnamectl set-hostname k8s-policy-01ufw allow 22/tcp
ufw allow 6443/tcp # k3s API server
ufw allow 80,443/tcp # ingress
ufw allow 10250/tcp # kubelet metrics
ufw --force enablecat <<EOF > /etc/sysctl.d/99-kubernetes.conf
fs.inotify.max_user_instances = 1024
fs.inotify.max_user_watches = 524288
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
modprobe br_netfilter
echo "br_netfilter" > /etc/modules-load.d/br_netfilter.conf
sysctl --systemDefault Ubuntu inotify values run out fast once Kyverno's controllers start watching CRDs.
Install k3s
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.32.4+k3s1 sh -s - \
--write-kubeconfig-mode=644 \
--disable-cloud-controller \
--kubelet-arg=eviction-hard=memory.available<200Mi,nodefs.available<10% \
--kubelet-arg=system-reserved=memory=300Mi,cpu=200m \
--kubelet-arg=kube-reserved=memory=300Mi,cpu=200mThe reserved/eviction flags are critical on 4 GB. Without them the kubelet has no headroom and will start killing system pods first.
systemctl status k3s
k3s kubectl get nodes
k3s kubectl get pods -A
mkdir -p ~/.kube
cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/configInstall Helm
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor -o /usr/share/keyrings/helm.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" \
> /etc/apt/sources.list.d/helm-stable-debian.list
apt update
apt install -y helm
helm versionThe raw YAML manifest works for testing, but the chart is the only path that exposes the configuration knobs you actually need.
Install Kyverno (Standalone Profile)
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm search repo kyverno/kyverno --versions | head -5# Standalone single-node profile for a budget VPS
admissionController:
replicas: 1
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 384Mi }
backgroundController:
replicas: 1
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 256Mi }
reportsController:
replicas: 1
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 256Mi }
cleanupController:
replicas: 1
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 128Mi }
# Exclude system namespaces so a webhook outage doesn't lock you out
config:
resourceFiltersExcludeNamespaces:
- kube-system
- kube-public
- kube-node-lease
- kyverno
- cert-manager
features:
admissionReports: { enabled: true }
policyReports: { enabled: true }
webhooksCleanup:
enable: truehelm install kyverno kyverno/kyverno \
--namespace kyverno --create-namespace \
--version 3.7.0 \
-f values.yaml
kubectl -n kyverno rollout status deployment/kyverno-admission-controller
kubectl -n kyverno rollout status deployment/kyverno-background-controller
kubectl -n kyverno rollout status deployment/kyverno-reports-controller
kubectl -n kyverno rollout status deployment/kyverno-cleanup-controllerIf the admission controller crashes on first start with a TLS error, give it 60s — the chart bootstraps its own CA on the first pass.
Apply Your First Policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-team-label
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Every pod must carry an 'app.kubernetes.io/managed-by' label."
pattern:
metadata:
labels:
app.kubernetes.io/managed-by: "?*"kubectl apply -f require-labels.yaml
# Should be rejected
kubectl run test --image=nginx
# Should be admitted
kubectl run test --image=nginx \
--labels=app.kubernetes.io/managed-by=manual
kubectl delete pod test
kubectl delete clusterpolicy require-labelsPod Security Standards via the Policies Chart
Install in audit mode first so you can see what would break before turning enforcement on.
helm install kyverno-policies kyverno/kyverno-policies \
--namespace kyverno \
--set podSecurityStandard=baseline \
--set validationFailureAction=Audit
kubectl get clusterpolicyreports
kubectl get policyreports -Ahelm upgrade kyverno-policies kyverno/kyverno-policies \
--namespace kyverno \
--set podSecurityStandard=baseline \
--set validationFailureAction=EnforceThe restricted profile is significantly stricter and will reject most off-the-shelf Helm charts unmodified. Stay on baseline until you have time to deal with the fallout.
Policy Reporter UI (Optional)
helm repo add policy-reporter https://kyverno.github.io/policy-reporter
helm repo update
helm install policy-reporter policy-reporter/policy-reporter \
--namespace policy-reporter --create-namespace \
--set kyvernoPlugin.enabled=true \
--set ui.enabled=true \
--set ui.plugins.kyverno=trueapiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: policy-reporter-ui
namespace: policy-reporter
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
rules:
- host: policy.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: policy-reporter-ui
port:
number: 8080Tuning & Upgrades
- Drop the cleanup controller if you never use cleanup policies — saves ~50 MB. Set
cleanupController.enabled=false. - Watch the reports controller — on busy clusters with ephemeral pods,
policyreportsaccumulate quickly. Prune via cron. - Tune webhook timeouts — default 10s. Lower to 5s via
admissionController.tuning.webhookTimeoutSecondsif the API server complains about latency. - Pin image versions in your values file before any production cutover.
helm repo update
helm upgrade kyverno kyverno/kyverno \
--namespace kyverno \
--version <new-version> \
-f values.yaml
helm upgrade kyverno-policies kyverno/kyverno-policies \
--namespace kyverno \
--version <new-version>CVE-2026-4789 disabled HTTP by default in namespaced policies. Upgrading from 1.16 or earlier? Audit any namespaced Policy that fetches data over HTTP — those calls now require explicit opt-in.
Webhook Lockout Recovery
Every Kyverno operator should rehearse this. If all Kyverno pods are unavailable (OOM, node reboot, image pull failure), the webhooks will reject every admission request — including the ones needed to bring Kyverno back up.
kubectl delete validatingwebhookconfiguration \
kyverno-policy-validating-webhook-cfg \
kyverno-resource-validating-webhook-cfg \
kyverno-cleanup-validating-webhook-cfg \
kyverno-exception-validating-webhook-cfg \
kyverno-global-context-validating-webhook-cfg \
kyverno-ttl-validating-webhook-cfg
kubectl delete mutatingwebhookconfiguration \
kyverno-policy-mutating-webhook-cfg \
kyverno-resource-mutating-webhook-cfg \
kyverno-verify-mutating-webhook-cfgOnce the admission controller is healthy again it re-registers the webhooks automatically. Excluding kube-system and the kyverno namespace (in your values file) prevents this in most realistic failure modes.
Troubleshooting Checklist
kubectl -n kyverno get pods— are all four controllers running?kubectl -n kyverno logs deploy/kyverno-admission-controller --tail=200— TLS or webhook registration errors?kubectl get validatingwebhookconfigurations -o yaml | grep -A2 failurePolicy— on a single-node setup,Failwill lock you out during restarts.kubectl describe clusterpolicy <name>— Kyverno reports rule compilation failures here.kubectl get events -A --sort-by='.lastTimestamp' | tail -50— admission rejections show up here with policy name and resource.
If the admission controller pod will not start at all, 90% of the time it is a memory limit set too low. Bump it to 512Mi and try again.
Backup Considerations
- k3s state — embedded SQLite at
/var/lib/rancher/k3s/server/db/. Stop k3s, snapshot, restart. - Policy YAMLs — should live in git. If not:
kubectl get clusterpolicies,policies -A -o yaml > policies-backup.yaml. - Helm values files — should also live in git.
