🗄️ Projet archivé — juin 2026. L’app
claude-webuia été décommissionnée (projet abandonné) et entièrement retirée du cluster. Cet article est conservé à titre historique.
Il y a quelque chose d’intrinsèquement comique dans le fait de demander à un agent IA d’ajouter un service Kubernetes, de le voir éditer les YAML du repo, pusher sur actual-state, et regarder ArgoCD déployer le résultat — sachant que cet agent tourne lui-même en pod sur ce même cluster. Il se déploie, au sens propre, lui-même.
C’est exactement la situation dans laquelle je me trouve depuis que j’ai mis en service claude-webui sur mon homelab k3s. Voici comment ça marche, ce que ça a fallu bricoler, et pourquoi c’est à la fois une bonne idée et légèrement inquiétant.
actual-state,
et ArgoCD le redéploie — le pod redémarre avec la config qu'il vient d'écrire.
La boucle se referme sur elle-même. The agent edits its own manifests, pushes to actual-state,
and ArgoCD redeploys it — the pod restarts with the config it just wrote.
The loop closes on itself. L’app : claude-code-webui
claude-code-webui est un wrapper web open-source autour de Claude Code, le CLI Anthropic. Il expose une interface navigateur qui lance des sessions Claude Code dans un répertoire de travail, transmet les prompts en streaming, et affiche les outils utilisés en temps réel. Pas d’API custom, pas de prompt engineering — c’est littéralement claude-code-webui --host 0.0.0.0 --port 8080 qui démarre le CLI en sous-process.
L’image tourne sur ghcr.io/yellowbeanie/claude-webui (privée, buildée via GitHub Actions sur ARM64 pour les nœuds du cluster). L’interface est exposée à claude.olig-homelab-2025.win, derrière un Cloudflare Tunnel et une politique Cloudflare Access restreinte à mon adresse Gmail. Sans ça, n’importe qui atteignant l’URL aurait un shell Claude Code sur le repo. Non négociable.
L’app ArgoCD qui orchestre tout ça est minimaliste :
# argocd/apps/claude-webui.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: claude-webui
namespace: argocd
spec:
source:
repoURL: [email protected]:YellowBeanie/k3s-homelab.git
targetRevision: actual-state
path: claude-webui
destination:
server: https://kubernetes.default.svc
namespace: claude-webui
syncPolicy:
automated:
prune: false
selfHeal: true
syncOptions:
- ServerSideApply=true
prune: false est délibéré — je ne veux pas qu’ArgoCD supprime des ressources si Claude pousse un manifest mal formé. La boucle de feedback est déjà assez tendue comme ça.
OAuth Pro : le token Anthropic dans un SealedSecret
Claude Code s’authentifie via un token Anthropic stocké dans ~/.claude/.credentials.json. Pour un pod éphémère, cette donnée doit être injectée de l’extérieur — et vu que le repo est public, elle doit être chiffrée.
La solution : un SealedSecret. Le fichier .credentials.json est scellé avec le certificat public du contrôleur Sealed Secrets du cluster, et le chiffré est commis dans le repo en toute sécurité. Seul le contrôleur peut le déchiffrer.
# Sceller les credentials OAuth depuis la machine locale
kubectl create secret generic claude-webui-oauth-creds \
--namespace claude-webui \
--from-file=.credentials.json="$HOME/.claude/.credentials.json" \
--dry-run=client -o yaml \
| kubeseal \
--cert docs/sealed-secrets/sealed-secrets-pub.pem \
--format yaml \
> claude-webui/sealedsecret-oauth-creds.yaml
Le résultat est un blob chiffré inoffensif qui part dans le repo :
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: claude-webui-oauth-creds
namespace: claude-webui
spec:
encryptedData:
.credentials.json: AgANgjWsBAA7ZC/NIkSH... # ~3 KB de chiffrement RSA-OAEP
Piège numéro un : Claude Code rafraîchit son token automatiquement et réécrit .credentials.json à chaque refresh. Si le fichier est monté directement depuis un Secret Kubernetes (read-only), le refresh échoue et l’agent se retrouve déconnecté en plein milieu d’un changement. La solution est d’utiliser le Secret uniquement comme graine initiale, via un init container qui copie le fichier sur un PVC si et seulement s’il n’existe pas encore. Le token live réside ensuite sur le volume persistant, pas dans le Secret.
La rotation se fait en deux commandes : claude logout && claude login localement, puis rescellage du nouveau .credentials.json. C’est arrivé plus d’une fois dans l’historique du repo — une fois parce que j’avais oublié que l’ancien token était encore référencé dans un chat et s’était retrouvé exposé. Rotation préventive.
Persistance : deux PVCs Longhorn
Sans persistance, chaque redémarrage du pod repart de zéro : pas d’historique de conversations, pas de contexte projet, le token OAuth potentiellement perdu. Deux PVCs Longhorn couvrent les deux besoins distincts :
# 1 Go pour ~/.claude — credentials, projets, historique
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claude-webui-home
namespace: claude-webui
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
storageClassName: standard-storage
---
# 5 Go pour /workspace — le clone du repo k3s-homelab
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claude-webui-workspace
namespace: claude-webui
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
storageClassName: standard-storage
Piège numéro deux : claude-code-webui lit ~/.claude.json (l’index des projets Claude) à chaque requête /projects et plante avec ENOENT si le fichier est absent. Il faut l’initialiser avant le premier démarrage du container principal. Et ce n’est pas tout.
Piège numéro trois : l’interface web filtre les projets affichés à ceux dont il existe un dossier correspondant sous ~/.claude/projects/<encoded>/. L’encodage transforme les /, \, :, . et _ en tirets. Le chemin /workspace/k3s-homelab devient donc -workspace-k3s-homelab. Si ce dossier n’existe pas, le projet n’apparaît pas dans l’UI — même si claude.json le référence correctement (commits 9a0c2dc et 7196a52).
Ces deux problèmes sont résolus dans l’init container seed-claude-creds :
# Extrait de l'init container seed-claude-creds
if [ ! -f /home/node/.claude/claude.json ]; then
echo '{"projects":{"/workspace/k3s-homelab":{}}}' \
> /home/node/.claude/claude.json
fi
mkdir -p /home/node/.claude/projects/-workspace-k3s-homelab
Et dans le container principal, ~/.claude.json est un symlink vers la copie persistante sur le PVC :
ln -sf /home/node/.claude/claude.json /home/node/.claude.json
exec claude-code-webui --host 0.0.0.0 --port 8080
SSH vers le master : kubectl sans ServiceAccount
Pour qu’un agent qui édite des YAML soit vraiment utile, il doit aussi pouvoir observer l’état du cluster — vérifier si un pod est en CrashLoopBackOff, inspecter des logs, valider qu’un déploiement a réussi. Deux options :
- Un ServiceAccount Kubernetes avec un Role RBAC exhaustif.
- Une clé SSH vers
[email protected](master-1) etsudo kubectl.
J’ai choisi la seconde. La raison honnête : écrire un Role RBAC couvrant tous les verbes dont Claude pourrait avoir besoin est un travail de longue haleine, et dans un homelab perso mono-utilisateur, le gain en isolation ne compense pas le coût de maintenance. Le risque est sciemment accepté.
La clé SSH (claude-cluster-runner@claude-webui, ed25519) est scellée comme les autres :
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: claude-webui-master-ssh-key
namespace: claude-webui
spec:
encryptedData:
id_ed25519: AgCztnTzUzFYl8yX... # clé ed25519 chiffrée
La configuration SSH dans un ConfigMap mappe l’alias master-1 vers l’IP réelle et l’identité correcte :
# configmap-ssh.yaml
data:
config: |
Host master-1
HostName 10.0.0.11
User admin
IdentityFile /home/node/.ssh/id_master_ed25519
IdentitiesOnly yes
StrictHostKeyChecking yes
UserKnownHostsFile /home/node/.ssh/known_hosts
L’init container clone-repo installe les deux clés (GitHub + master) dans un emptyDir monté en ~/.ssh, avec les permissions 0600 requises. Le container principal monte ce même répertoire en lecture seule — une précaution pour éviter qu’une session Claude Code rogue n’écrase ou n’exfiltre les clés via l’interface web.
Depuis l’UI, Claude peut désormais faire :
ssh master-1 sudo kubectl -n argocd get apps
ssh master-1 sudo kubectl -n monitoring logs -l app=grafana --tail=50
Deploy key GitHub : pusher sans token humain
Pour que Claude puisse commiter et pusher les manifests modifiés sur actual-state, une deploy key avec accès en écriture est enregistrée sur le repo YellowBeanie/k3s-homelab. Elle est scellée et injectée de la même manière que la clé master.
L’init container clone-repo fait un clone (ou un git reset --hard origin/actual-state) à chaque démarrage du pod. Le reset dur est intentionnel : le workspace repart toujours d’un état propre aligné sur GitHub, même si Claude avait laissé des fichiers non commités lors du restart précédent.
Les variables d’environnement Git dans le Deployment signent les commits avec une identité reconnaissable :
env:
- name: GIT_AUTHOR_NAME
value: "Claude (homelab)"
- name: GIT_AUTHOR_EMAIL
value: "[email protected]"
Le Deployment complet
Pour avoir une vue d’ensemble, voici le Deployment avec ses volumes (légèrement abrégé) :
apiVersion: apps/v1
kind: Deployment
metadata:
name: claude-webui
namespace: claude-webui
spec:
replicas: 1
strategy:
type: Recreate # pas de rolling update : ReadWriteOnce sur les PVCs
template:
spec:
imagePullSecrets:
- name: claude-webui-ghcr # GHCR privé
securityContext:
fsGroup: 1000
runAsUser: 1000
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
initContainers:
- name: seed-claude-creds # graine OAuth + claude.json + dossier encodé
...
- name: clone-repo # installe les clés SSH, clone/reset le repo
...
containers:
- name: claude-webui
image: ghcr.io/yellowbeanie/claude-webui:latest
command: ["/usr/bin/tini", "--"]
args:
- /bin/sh
- -c
- |
ln -sf /home/node/.claude/claude.json /home/node/.claude.json
exec claude-code-webui --host 0.0.0.0 --port 8080
resources:
requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "2", memory: 2Gi}
volumeMounts:
- name: workspace
mountPath: /workspace
- name: claude-home
mountPath: /home/node/.claude
- name: ssh-dir
mountPath: /home/node/.ssh
readOnly: true # clés SSH montées en lecture seule au runtime
volumes:
- name: workspace
persistentVolumeClaim:
claimName: claude-webui-workspace # 5 Gi, repo k3s-homelab
- name: claude-home
persistentVolumeClaim:
claimName: claude-webui-home # 1 Gi, ~/.claude
- name: ssh-dir
emptyDir: {} # rempli par l'init container
- name: claude-creds-secret
secret:
secretName: claude-webui-oauth-creds
- name: deploy-key-secret
secret:
secretName: claude-webui-deploy-key
- name: master-key-secret
secret:
secretName: claude-webui-master-ssh-key
GHCR privé
L’image ghcr.io/yellowbeanie/claude-webui est privée (elle embarque un Dockerfile custom pour ajuster les chemins et dépendances sur ARM64). Le pattern d’accès est le même que pour les autres namespaces du cluster : un SealedSecret claude-webui-ghcr de type kubernetes.io/dockerconfigjson, référencé dans imagePullSecrets. L’image est épinglée par digest SHA256 via le workflow GitHub Actions qui commite automatiquement le digest après chaque build.
Risques et garde-fous
Faire tourner un agent IA avec un accès en écriture au repo d’infra et un sudo sur le master, c’est une décision qui mérite d’être documentée honnêtement.
Les risques concrets que j’ai rencontrés :
-
Boucles de retry. Claude n’a pas de notion de “backoff”. Si un
kubectl applyéchoue sur un manifest mal formé, il peut enchaîner les tentatives sans s’arrêter. La solution : branch protections surmain(les merges requièrent une review humaine), et je surveille les notifications GitHub pour les pushes trop fréquents suractual-state. -
Token OAuth qui expire. Le refresh automatique fonctionne tant que le processus tourne. Après un redémarrage du pod, si le token sur le PVC est périmé, l’agent démarre mais échoue à la première requête à l’API Anthropic. Symptôme : l’UI affiche une erreur 401 en streaming. Remède : reseal.
-
L’agent qui touche à ses propres manifests. C’est possible — et ça s’est produit. Claude a modifié
deployment.yamlsur instruction pour ajouter une variable d’environnement. ArgoCD a déployé la modification. Le pod a redémarré avec la nouvelle config. Tout s’est bien passé. Mais ça veut dire qu’une instruction mal formulée peut provoquer un redémarrage du pod en cours de conversation. Prévoir de reformuler.
Les garde-fous en place :
- Cloudflare Access sur l’ingress : seul mon Gmail peut ouvrir l’UI.
- Branch protections sur
main: pas de push direct, les PRs critiques passent par une review humaine. prune: falsedans la politique ArgoCD : ArgoCD ne supprime pas de ressources si le manifest disparaît.- Clés SSH montées en lecture seule dans le container runtime.
automountServiceAccountToken: false: le pod n’a pas de token ServiceAccount en mémoire.
L’effet en pratique
Depuis la mise en service, environ 80% des changements d’infrastructure du repo passent par l’interface Claude. Les cas d’usage les plus fréquents : ajouter une annotation, ajuster une ressource requests/limits, investiguer un pod en erreur, rédiger un SealedSecret pour un nouveau namespace, écrire un article de blog sur le déploiement d’agents IA dans des clusters Kubernetes.
Ce dernier point n’est pas une blague. Une partie de cet article a été rédigée depuis l’interface claude.olig-homelab-2025.win, dans le workspace /workspace/k3s-homelab, par un pod qui tourne sur le cluster décrit dans l’article.
La mise en abyme s’arrête là. Pour l’instant.
Voir aussi
Auditer la sécurité de mon homelab avec un agent IA (Claude Fable 5) — même logique de garde-fous (preuves, barrières, retour arrière) appliquée cette fois à un audit de sécurité complet du cluster.