Skip to content

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):

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 named longhorn exists, it is used. Else the StorageClass marked default. Else local-path.
  • Longhorn is what we recommend on a real homelab: replicated block, CSI volume snapshots, disks that survive losing a worker. local-path is 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) fills cidr / gateway / ipPool / dns from 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/24 and VMs should sit on VLAN 10.0.50.0/24, auto is wrong — set the VLAN explicitly and autoFromCluster: false (see values-homelab.yaml).
  • Auto never sets bridge.uplink. Enslaving the Kubernetes NIC drops SSH. Public still needs a second NIC, a VLAN iface, or an existing br0 before you flip enabled: 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.