Permissões de Volume e fsGroup
Objective
Muitas imagens rodam como usuário não root, e clusters endurecidos exigem isso. Quando um container desses monta um PersistentVolume recém-provisionado, o diretório raiz do volume muitas vezes pertence ao root com modo 0755, e a primeira escrita falha com Permission denied. A resposta do Kubernetes é o securityContext do Pod: runAsUser, runAsGroup e, principalmente, fsGroup, que faz o kubelet entregar o volume a um grupo do qual o container faz parte. Se você precisa dele depende do backend de storage, e é por isso que um manifest que funciona num cluster falha em outro. Este conceito cobre como o fsGroup funciona, quando ele se aplica, e o que fazer quando não se aplica.
Use Cases
- Um Pod em
CrashLoopBackOffcomPermission deniedno diretório de dados logo depois de ganhar um PVC. - Rodar workloads stateful com
runAsNonRootsob uma políticarestrictedde Pod Security. - Pods demorando para subir em volumes grandes por causa de mudanças recursivas de dono.
- Levar manifests de um laboratório (storage hostpath) para armazenamento em bloco de verdade.
Deep Dive
Quem é o container?
plaintextkubectl exec <pod> -- id # uid=1000 gid=1000 groups=1000 docker image inspect <imagem> --format '{{.Config.User}}'
O USER da imagem decide o padrão; securityContext.runAsUser/runAsGroup o sobrescrevem. IDs numéricos é que importam, não nomes: o nó e o volume só enxergam números.
fsGroup
plaintextspec: securityContext: runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch containers: - name: app image: registry.example.com/demo/web-app:1.4.2 volumeMounts: [{name: data, mountPath: /data}] volumes: - name: data persistentVolumeClaim: {claimName: app-data}
Para tipos de volume que suportam gerenciamento de dono, o kubelet, antes de subir os containers:
- muda o grupo dos arquivos do volume para o
fsGroup(recursivamente), - adiciona permissões de leitura e escrita para o grupo e o bit setgid nos diretórios, para que arquivos novos herdem o grupo,
- adiciona o
fsGroupaos grupos suplementares de cada processo de container.
O processo agora consegue escrever, como membro do grupo, não importa qual UID seja dono do diretório. Verificado no k3s: com fsGroup: 10001 e runAsUser: 10001, um arquivo criado num emptyDir aparece com dono 10001 e grupo 10001, e o id lista 10001 entre os grupos.
Quando o fsGroup não faz nada
O gerenciamento de dono depende do plugin de volume:
- Aplica: volumes baseados em bloco (discos de nuvem, Longhorn, Ceph RBD, volumes local, drivers CSI que declaram
fsGroupPolicy: File),emptyDir,configMap/secret/projected(grupo aplicado aos arquivos). - Não aplica:
hostPath, e drivers CSI comfsGroupPolicy: None. NFS depende do driver e do servidor (root squashing costuma impedir).
O hostpath-storage do MicroK8s provisiona PersistentVolumes hostPath. Verificado no MicroK8s 1.35 com um Pod busybox rodando como UID 1000:
plaintextuid=1000 gid=1000 groups=1000 drwxrwxrwx 2 root root 4096 /data WRITE_OK
O diretório provisionado tem modo 0777, então a escrita funciona com ou sem fsGroup, e com fsGroup: 1000 o diretório continua root:root. Ele "simplesmente funciona" porque o diretório é gravável por todos, não por causa do fsGroup. O provisioner local-path do k3s se comporta do mesmo jeito.
É assim que acontece a surpresa clássica: um manifest funciona por meses num provisioner estilo hostpath, depois vai para um cluster com armazenamento em bloco de verdade, onde a raiz do volume ext4 novo é root:root 0755 (mais um diretório lost+found), e a aplicação falha ao subir. Definir o fsGroup desde o primeiro dia torna o manifest portável.
Inicialização lenta: fsGroupChangePolicy
O chown/chmod recursivo roda a cada montagem. Num volume com milhões de arquivos, ele pode atrasar a subida do container em minutos, tempo suficiente para estourar os limites da startup probe.
plaintextsecurityContext: fsGroup: 1000 fsGroupChangePolicy: OnRootMismatch # pula a varredura se o diretório raiz já tem o grupo e o modo certos
OnRootMismatch verifica só o diretório de topo; Always (o padrão) percorre tudo a cada vez.
Quando o software espera ser dono do diretório
Alguns softwares verificam o dono em vez das permissões e recusam um diretório de dados que não pertence a eles, ou um com permissões de grupo. Imagens desses softwares muitas vezes sobem como root, fazem chown no diretório e trocam para um usuário sem privilégios no entrypoint. Isso exige que o container suba como root, o que conflita com runAsNonRoot.
Opções para softwares assim:
- Manter o entrypoint root da imagem e endurecer em outros pontos (capabilities, seccomp, NetworkPolicies).
- Rodar como o usuário final e apontar o diretório de dados para um subdiretório que o próprio processo cria (por exemplo
/data/app), de modo que o dono da raiz da montagem não importe desde que ela seja gravável, o que ofsGroupgarante. - Um
initContainerrodando como root que fazchownno volume uma vez. Funciona, e é exatamente o que a Pod Securityrestrictedproíbe, então prefira as opções acima.
Diagnosticando
plaintextkubectl logs <pod> --previous | grep -iE 'permission|denied|read-only' kubectl exec <pod> -- sh -c 'id; ls -ldn /data' kubectl get pv <pv> -o jsonpath='{.spec.csi.driver}{.spec.hostPath.path}{"\n"}' # qual backend?
Compare o UID e os grupos do processo com o dono, o grupo e o modo numéricos do diretório. Se o Pod cai rápido demais para um exec, sobrescreva temporariamente o comando com sleep 3600 para inspecionar.
Trade-offs
- fsGroup vs storage gravável por todos. O
fsGroupdá acesso a exatamente um grupo e funciona em armazenamento em bloco de verdade; depender de diretórios0777só funciona em provisioners estilo hostpath e dá permissão de escrita a todo mundo no nó. AlwaysvsOnRootMismatch.Alwayscorrige desvios de dono dentro do volume a cada subida;OnRootMismatchsobe muito mais rápido em volumes grandes e confia que nada mudou os donos por baixo.- Entrypoints root vs não root em tudo. Deixar que imagens que precisam subam como root é pragmático; não root estrito exige configuração específica de cada imagem, mas passa na Pod Security
restricted.