Chart values (defaults)¶
This page is the written default for the Helm charts. Helm only applies values at install or upgrade. Flags like --set must sit on the same helm install / helm upgrade command as the chart — they are not kubectl args and they are not remembered unless you pass --reuse-values on the next upgrade.
Full source in git (always in sync with the chart):
charts/virtfoundry/values.yaml— API + UIcharts/virtfoundry-operator/values.yaml— CRDs + operator
Why --set goes on the Helm command¶
| Mechanism | When it applies | Use for |
|---|---|---|
Chart values.yaml (below) |
Every install unless overridden | Safe defaults |
-f my-values.yaml |
Same Helm command | Homelab / GitOps overlays |
--set key=value |
Same Helm command | Secrets and one-off overrides |
kubectl edit / env on a pod |
After install, until the next Helm sync | Emergency only — Helm will overwrite |
--set secrets.rootPassword=… and --set secrets.jwtSecret=… belong next to helm install virtfoundry … because those keys live in the chart. A later kubectl set env is not the source of truth.
Prefer a file for anything larger than two secrets:
helm install virtfoundry virtfoundry/virtfoundry \
--version 0.7.2 \
--namespace virtfoundry-system \
-f my-values.yaml \
--set secrets.rootPassword='choose-a-strong-password' \
--set secrets.jwtSecret="$(openssl rand -hex 32)"
Those two keys have no defaults: the chart refuses to render without them (or
secrets.existingSecret). Rules and GitOps guidance: Secrets.
helm upgrade … --reuse-values keeps previous --set / -f unless you override them.
Longhorn (storage)¶
VirtFoundry does not install a disk backend. VM disks are Kubernetes PVCs on whatever StorageClass you point at.
- Chart key
platform.storage.defaultClass: auto(default): at install time Helm looks at the cluster. If a StorageClass namedlonghornexists, it is used. Else the StorageClass marked default. Elselocal-path. - Longhorn is what we recommend on a real homelab: replicated block, CSI volume snapshots, disks that survive losing a worker.
local-pathis a demo class — volume snapshots will fail. - You still install Longhorn before VirtFoundry (prerequisites). The chart only selects the class; it does not deploy Longhorn.
- Force a class:
--set platform.storage.defaultClass=longhorn(and usually--set platform.storage.snapshotClass=longhorn).
Host bridges (isolated / public)¶
platform.networking.isolated.enabled and platform.networking.public.enabled both default to false. A default install does not schedule the hostNetwork bridge DaemonSet. Set either flag only when you need Multus host bridges — that opt-in documents the host privileges in Configuration — host bridges.
Kind and full L2 overlays set isolated.enabled: true (and often bridge.tolerations: [{operator: Exists}] so the DaemonSet can run on a tainted control-plane).
Public IP (optional LAN / VLAN)¶
platform.networking.public.enabled defaults to false. Then VMs use the pod network. You do not need --set for public IPs to install the control plane.
When public is on, each VM can get a second NIC on a Linux bridge (vf-pub0 by default) and a static address from ipPool (cloud-init). That CIDR must be your LAN or VLAN — not a number we invented.
Homelab, you did not pass CIDR/gateway:
autoFromCluster: true(chart default) fillscidr/gateway/ipPool/dnsfrom the first Kubernetes Node InternalIP (/24, gateway.1, pool.20–.80).- That InternalIP is the kubelet address. If Kubernetes lives on
10.0.30.0/24and VMs should sit on VLAN10.0.50.0/24, auto is wrong — set the VLAN explicitly andautoFromCluster: false(seevalues-homelab.yaml). - Auto never sets
bridge.uplink. Enslaving the Kubernetes NIC drops SSH. Public still needs a second NIC, a VLAN iface, or an existingbr0before you flipenabled: true.
Generate a values snippet from this host or kubectl:
./scripts/detect-host-public-net.sh > public-from-host.yaml
# edit gateway / pool, then:
helm upgrade --install virtfoundry virtfoundry/virtfoundry -n virtfoundry-system \
-f public-from-host.yaml \
--set secrets.rootPassword='…' \
--set secrets.jwtSecret='…'
Details: Configuration — public networking, Topologies.
Default values — virtfoundry (API + UI)¶
# Default values for VirtFoundry.
# Installs API + UI. Platform state in virtfoundry.io CRDs (requires virtfoundry-operator).
nameOverride: ""
fullnamePrefix: virtfoundry
namespace: virtfoundry-system
store:
driver: kubernetes
images:
api: ghcr.io/virtfoundry/core:0.7.3
ui: ghcr.io/virtfoundry/ui:0.7.3
pullPolicy: IfNotPresent
# Optional: ["ghcr-pull"] for private GHCR packages
pullSecrets: []
# Bootstrap credentials for the API. The chart ships no defaults on purpose:
# rendering fails with an explicit error until real values are supplied.
secrets:
# Recommended for GitOps: reference a Secret you created out of band (Sealed
# Secrets, External Secrets, SOPS). When set, the chart does not manage the
# Secret and the inline values below are ignored.
existingSecret: ""
rootPasswordKey: ROOT_PASSWORD
jwtSecretKey: JWT_SECRET
# Required unless existingSecret is set. rootPassword >= 12 chars,
# jwtSecret >= 32 chars (`openssl rand -hex 32`); the published defaults
# "virtfoundry" and "change-me-in-production" are rejected.
rootPassword: ""
jwtSecret: ""
# Generate a random 48-char jwtSecret on first install and keep it on upgrade by
# reading the live Secret back with `lookup`. Needs an API server connection, so
# `helm template` and client-side dry-runs still fail closed — Argo CD and other
# GitOps flows should use existingSecret instead.
autoGenerateJwtSecret: false
# Local development only: skip the checks above and set
# VF_ALLOW_INSECURE_DEFAULTS=1 on the API (core#108). Never enable on a cluster
# that is reachable from outside your laptop.
allowInsecureDefaults: false
config:
logLevel: info
jwtExpire: 86400
kubevirtEnabled: true
# Off by default: a cleartext HTTP control plane is not a safe default (passwords/JWTs).
# Enable with ingress.tls (and optional cert-manager annotations), or set
# ingress.allowCleartext=true for lab-only HTTP. See docs/guide/configuration.md#ingress-and-tls
ingress:
enabled: false
className: nginx
host: iaas.local
# When true, allows ingress.enabled without ingress.tls (lab only).
allowCleartext: false
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# cert-manager (optional) — pair with ingress.tls[].secretName:
# cert-manager.io/cluster-issuer: letsencrypt-prod
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Required when enabled (unless allowCleartext). Example:
# tls:
# - secretName: virtfoundry-tls
# hosts:
# - iaas.example.com
tls: []
# Gateway API HTTPRoute (mutually exclusive with ingress.enabled).
# Default parentRefs.sectionName is websecure (HTTPS listener). Pair with an
# HTTP→HTTPS redirect on sectionName web — see values-gateway.yaml and
# docs/examples/httproute-https-redirect.yaml.
gateway:
enabled: false
parentRefs:
- name: platform-gateway
namespace: traefik
sectionName: websecure
hostnames:
- iaas.local
ui:
replicas: 1
# Container listens on 8080 as non-root (chart mounts nginx ConfigMap; Service stays 80→8080).
# Requires UI image that can run as UID 101, or the ConfigMap override (default).
containerPort: 8080
service:
type: ClusterIP
port: 80
nodePort: 30880
securityContext:
runAsUser: 101 # nginx user in nginx:alpine / ghcr.io/virtfoundry/ui
runAsGroup: 101
fsGroup: 101
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
api:
service:
type: ClusterIP
port: 8080
replicas: 1
securityContext:
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
# Browser origins allowed for CORS and for /ws/events + /ws/console.
# The request host is always accepted (UI nginx proxies /api and /ws same-origin),
# so leave empty for the default Ingress/Gateway layout. Set this only when the
# UI is served from a different origin than the API (split UI/API). Maps to
# security.allowed_origins / VIRTFOUNDRY_ALLOWED_ORIGINS in core.
security:
allowedOrigins: []
# Example for split origins:
# allowedOrigins:
# - "https://console.example.com"
# Control-plane NetworkPolicy (API + UI). Empty allowedIngressNamespaces = any
# namespace may reach the HTTP ports (safe default for Ingress/Gateway). Set the
# list (e.g. ingress-nginx, traefik) to tighten. API egress stays open for the
# kube-apiserver. Disable with networkPolicy.enabled=false on CNIs without NP.
networkPolicy:
enabled: true
allowedIngressNamespaces: []
# Example:
# allowedIngressNamespaces:
# - ingress-nginx
# - traefik
rbac:
api:
# Namespaces where the API may manage Secrets. The release namespace is always
# included. Listing your tenant namespaces (virtfoundry-tenant-{slug}) here
# removes the cluster-wide Secret rule from the API ClusterRole entirely — the
# strictest setup, at the cost of a values change per new tenant.
# Leave empty to keep the cluster-scoped fallback (get/create/update only,
# never list/watch/delete). See README "API permissions".
secretNamespaces: []
# Cluster-side guard rail: denies the API ServiceAccount any Namespace DELETE
# outside virtfoundry-tenant-* / virtfoundry-vpc-* carrying a virtfoundry.io/
# label. Rendered only when the cluster serves ValidatingAdmissionPolicy
# (Kubernetes >= 1.30).
namespaceGuard:
enabled: true
# Optional cluster/platform bootstrap (bare-metal / air-gapped without a registry).
# Image sideloading stays outside the chart — see scripts/sideload/.
platform:
multus:
# Install Multus thick plugin from upstream manifest when absent.
# The installer Job applies upstream RBAC, so its hook ServiceAccount holds
# escalate/bind for the pre-install phase. Keep false when Multus is managed
# outside the chart (scripts/setup/multus.sh or your platform team).
install: false
manifestUrl: https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset-thick.yml
networking:
# Public/shared network for tenant VMs (Multus bridge or macvlan — configured per site).
public:
enabled: false
# When true, empty/"auto" cidr, gateway, and ipPool are filled from the first
# Node InternalIP (/24, gateway .1, pool .20–.80). Never sets uplink.
autoFromCluster: true
mode: bridge # bridge | macvlan
cidr: 10.0.50.0/24
gateway: 10.0.50.254
dns:
- 10.0.50.254
ipPool:
start: 10.0.50.10
end: 10.0.50.99
reservedRanges:
- start: 10.0.50.100
end: 10.0.50.150
reason: metallb-loadbalancer
bridge:
# Linux IFNAMSIZ is 16 incl. NUL → max 15 chars. "virtfoundry-pub0" is 16 and fails.
name: vf-pub0
uplink: "" # physical NIC or VLAN interface on the node
address: "" # optional bridge IPv4 for dnsmasq (outside VM pool)
macvlan:
master: ""
mode: bridge
nad:
name: virtfoundry-public
namespace: virtfoundry-system
# Private tenant VPC networks (isolated L2 on internal bridge).
# Default false: API+UI-only installs must not schedule a hostNetwork bridge
# DaemonSet. Opt in (with public.enabled) only when you need Multus host bridges —
# see docs/guide/configuration.md#host-bridges-isolated--public.
isolated:
enabled: false
bridge:
name: virtfoundry-br0
# Default VM networking when no network_ids are set on deploy.
vm:
defaultNetwork: pod # pod | public
allowPodNetwork: true # false = Multus-only when public network is used
bridge:
namespace: kube-system # DaemonSet namespace for bridge setup
repairIntervalSeconds: 15 # bridge-keeper auto-repair loop
# alpine:3.21 index digest (multi-arch). Override with a prebuilt image that
# already includes dnsmasq to avoid runtime `apk add` in the dhcp container.
image: alpine@sha256:ce64758a109eb420d874a118f87920e625e12d3634e03b4a5573fd9f6e5d3507
# Empty = do not tolerate control-plane NoSchedule. Homelab / kind overlays that
# run bridges on every node (including control-plane) should set operator: Exists.
nodeSelector: {}
tolerations: []
storage:
# auto = Longhorn if the StorageClass exists, else the cluster default, else local-path.
# helm template / CI (no cluster) falls back to local-path.
defaultClass: auto
# VolumeSnapshotClass. Empty = longhorn when defaultClass resolves to longhorn, else cluster default.
snapshotClass: ""
windowsBootSizeGi: 32
windowsISOSizeGi: 8
kubevirt:
# Guest vCPU overcommit for shared VMs (VirtFoundry omits CPU request).
# Pod request ≈ guest_vcpus / cpuAllocationRatio. 0 = do not patch CR.
# > 0 renders a post-install Job whose identity can only get/patch kubevirt/kubevirt.
cpuAllocationRatio: 10
featureGates:
enabled: false
gates:
- Snapshot
- VideoConfig
cdi:
# Same hook RBAC caveat as platform.multus.install: the Job applies the upstream
# operator manifests, which requires escalate/bind during pre-install.
install: false
version: "" # empty = latest from GitHub releases API at install time
# CDI HTTP importer pods run in each *tenant* namespace (DataVolume NS), not
# in this release namespace. Core creates NetworkPolicy
# virtfoundry-cdi-importer-egress there on EnsureTenantNamespace — a chart
# NetworkPolicy here cannot cover them. Docs + private-mirror example:
# docs/guide/features/templates.md#cdi-importer-egress
Default values — virtfoundry-operator¶
nameOverride: ""
fullnamePrefix: virtfoundry-operator
namespace: virtfoundry-system
image:
repository: ghcr.io/virtfoundry/operator
tag: "0.7.3"
digest: ""
pullPolicy: IfNotPresent
replicas: 1
leaderElection: true
# Cluster-side guard rail: denies the operator ServiceAccount any Namespace
# DELETE outside virtfoundry-tenant-* labelled virtfoundry.io/tenant. Rendered
# only when the cluster serves ValidatingAdmissionPolicy (Kubernetes >= 1.30).
namespaceGuard:
enabled: true
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# Homelab sideload: set pullPolicy IfNotPresent after ctr import.