5.5 KiB
ward — setup and wiring guide
ward runs on the k3s master as root, issues short-lived client certificates, and lets users fetch a ready-to-use kubeconfig (or act as a kubectl exec credential plugin) without anyone having to touch the k3s CA by hand.
1. Build and install
# On the k3s master (or cross-compile and copy)
go build -o /usr/local/bin/ward .
The same binary is both the server and the client-side exec plugin — subcommand dispatch happens on the first argument.
2. Server setup
Minimal (LDAP, auto-discovered via DNS SRV)
ExecStart=
ExecStart=/usr/local/bin/ward \
--k3s-server=https://k3s.example.com:6443 \
--addr=:8443 \
--ldap
With an explicit LDAP URI (no DNS SRV required)
/etc/systemd/system/ward.service.d/local.conf
[Service]
ExecStart=
ExecStart=/usr/local/bin/ward \
--k3s-server=https://k3s.example.com:6443 \
--addr=:8443 \
--ldap-uri=ldaps://ldap.example.com
systemctl daemon-reload
systemctl enable --now ward
With Kerberos SPNEGO
ExecStart=
ExecStart=/usr/local/bin/ward \
--k3s-server=https://k3s.example.com:6443 \
--addr=:8443 \
--kerberos \
--keytab=/etc/krb5.keytab \
--spn=HTTP/k3s.example.com
Add the service principal to the KDC
# On the KDC (MIT Kerberos)
kadmin.local -q "addprinc -randkey HTTP/k3s.example.com@EXAMPLE.COM"
kadmin.local -q "ktadd -k /etc/krb5.keytab HTTP/k3s.example.com@EXAMPLE.COM"
# Verify
klist -k /etc/krb5.keytab
For Samba AD / Heimdal, the equivalent is:
samba-tool spn add HTTP/k3s.example.com ward_service_account
net ads keytab add HTTP/k3s.example.com -U Administrator
With htpasswd (break-glass / service accounts)
ExecStart=
ExecStart=/usr/local/bin/ward \
--k3s-server=https://k3s.example.com:6443 \
--addr=:8443 \
--kerberos \
--htpasswd=/etc/ward/htpasswd
# Create the file and add a user (bcrypt is required for new entries)
mkdir -p /etc/ward
htpasswd -B -c /etc/ward/htpasswd alice
# Add more users
htpasswd -B /etc/ward/htpasswd bob
# Reload without restarting (SIGHUP)
systemctl reload ward
3. Get the bootstrap kubeconfig
The /bootstrap endpoint returns a ready-to-use kubeconfig with no embedded
credentials — just the cluster endpoint, server CA, and the exec plugin stanza
that tells kubectl to call ward credential on demand. No authentication
required to fetch it.
curl -s https://k3s.example.com:8443/bootstrap > ~/.kube/config
The username in the kubeconfig is resolved in priority order:
?user=query parameter — explicit override- Credentials in the request (Basic or Kerberos) — personalised on the fly
- The placeholder
YOUR_USERNAME_HERE— for anonymous fetches
# Personalised via query param (useful for scripting)
curl -s https://k3s.example.com:8443/bootstrap?user=alice > ~/.kube/config
# Personalised by authenticating while fetching
curl -su alice https://k3s.example.com:8443/bootstrap > ~/.kube/config
Distribute this URL to users alongside the ward binary. That's the entire
onboarding process.
4. Client setup
Each user needs:
- The
wardbinary in their$PATH - The bootstrap kubeconfig
# Install the binary (copy from the server, or build locally)
sudo cp ward /usr/local/bin/ward
# Install the kubeconfig
cp bootstrap-kubeconfig.yaml ~/.kube/config # or merge manually
That's it. First use:
# Kerberos path — if you have a valid ticket, it just works:
kinit alice@EXAMPLE.COM
kubectl get nodes
# Basic auth path — prompted on first use, then cached for 24 h:
kubectl get nodes
# Password for alice: ▌
Credentials are cached in ~/.cache/ward/ and reused until 5 minutes before
expiry. kubectl re-invokes the plugin automatically at that point — no manual
kubectl login step required.
5. RBAC setup
The issued certificate has:
- CN = username (LDAP
sAMAccountName/uid, or Kerberos principal primary) - O = LDAP group CNs (maps to Kubernetes RBAC groups)
Bind roles as usual:
# Grant a specific user cluster-admin
kubectl create clusterrolebinding alice-admin \
--clusterrole=cluster-admin \
--user=alice
# Grant an LDAP group view access
kubectl create clusterrolebinding k8s-readers \
--clusterrole=view \
--group=k8s-users
6. Debugging
Server-side
# Enable verbose logging
WARD_DEBUG=1 systemctl restart ward
journalctl -u ward -f
# Or pass the flag directly
ward --debug --k3s-server=... --addr=:8443
Server debug output shows:
- LDAP connection attempts (host, TLS, bind DN)
- Auth method selected per request (Kerberos / Basic)
- Group lookup results
- Cert issuance details
Client-side
# Inline for a single command
WARD_DEBUG=1 kubectl get nodes
# Or permanently in the kubeconfig exec env block:
# env:
# - name: WARD_DEBUG
# value: "1"
Client debug output (stderr, passed through by kubectl) shows:
- Which Kerberos credential cache was found and the principal in it
- Cache hit/miss and time remaining on cached cert
- HTTP request URL and response status
- Fallback to Basic auth and why
Bypass the cache
ward credential --server=https://k3s.example.com:8443 --no-cache --debug
Test without kubectl
# Kerberos
kinit alice
ward credential --server=https://k3s.example.com:8443 --debug | python3 -m json.tool
# Basic auth
ward credential --server=https://k3s.example.com:8443 --no-kerberos --debug