Files
ward/README.md
2026-03-02 15:19:32 +01:00

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:

  1. ?user= query parameter — explicit override
  2. Credentials in the request (Basic or Kerberos) — personalised on the fly
  3. 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:

  1. The ward binary in their $PATH
  2. 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