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

246 lines
5.5 KiB
Markdown

# 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
```bash
# 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)
```ini
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
```
```ini
[Service]
ExecStart=
ExecStart=/usr/local/bin/ward \
--k3s-server=https://k3s.example.com:6443 \
--addr=:8443 \
--ldap-uri=ldaps://ldap.example.com
```
```bash
systemctl daemon-reload
systemctl enable --now ward
```
### With Kerberos SPNEGO
```ini
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
```bash
# 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:
```bash
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)
```ini
ExecStart=
ExecStart=/usr/local/bin/ward \
--k3s-server=https://k3s.example.com:6443 \
--addr=:8443 \
--kerberos \
--htpasswd=/etc/ward/htpasswd
```
```bash
# 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.
```bash
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
```bash
# 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
```bash
# 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:
```bash
# 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:
```bash
# 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
```bash
# 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
```bash
# 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
```bash
ward credential --server=https://k3s.example.com:8443 --no-cache --debug
```
### Test without kubectl
```bash
# 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
```