Construir uma matriz de acesso verificável
Num serviço fictício de fecho de posições, o suporte precisa de consultar o estado de uma reconciliação, mas não de modificar o workload nem consultar dados de outros clientes. Traduz o pedido em atributos concretos: identidade, namespace, API group, recurso, subrecurso, verbo e, quando aplicável, nome. Uma linha da matriz pode permitir get no ConfigMap closing-status em funds; outra deve recusar get no ConfigMap other-status. Regista também quem aprova a concessão, quando termina e quem recolhe a evidência de retirada. Esta matriz torna a conversa entre APS, segurança e plataforma verificável antes de existir um manifesto. Segue o percurso da credencial até ao resultado. Um cliente pode falhar a ligação TLS antes de apresentar uma identidade; pode apresentar um token que a API recusa; ou pode autenticar-se e não ter autorização para o pedido. Mesmo uma operação autorizada pode falhar numa etapa posterior. Usa o código HTTP, a mensagem sanitizada e os atributos efetivos do pedido para localizar a fase. Não respondas a qualquer falha com um Role mais amplo. Na passagem para RUN, entrega exemplos permitidos e proibidos com os resultados esperados para a identidade da aplicação.
Separar identidade, âmbito e operação
Uma ServiceAccount pertence a um namespace, mas pode receber acesso noutro. Se ops/collector lê objetos em funds, o RoleBinding de destino existe em funds e o subject conserva namespace: ops. O nome curto collector, sozinho, não distingue as contas. Um ClusterRole também pode ser reutilizado por vários RoleBindings sem conceder automaticamente acesso global. Para recursos namespaced, é o binding que determina onde aquela concessão se aplica. Documenta esta diferença quando uma equipa pretende reutilizar um perfil aprovado em várias aplicações. Não colapses operações diferentes numa etiqueta de leitura. Ler um Pod não autoriza necessariamente ler pods/log. Listar uma coleção também difere de obter um objeto pelo nome. No exercício, o operador pode obter closing-status, mas a listagem sem filtro é recusada; a listagem com o campo metadata.name adequado funciona. O resultado ajuda a perceber porque um dashboard que enumera objetos pode falhar mesmo quando o diagnóstico manual por nome funciona. Decide se o cliente deve pedir apenas o objeto necessário ou se existe uma necessidade aprovada de listagem mais ampla. Um label selector não substitui uma restrição RBAC por nome.
Delegar sem perder o limite aprovado
Dar create em roles não significa permitir qualquer conjunto de regras. A API verifica se o autor possui as permissões que tenta incluir no mesmo âmbito, salvo autorização explícita de escalate. De forma semelhante, criar um RoleBinding exige uma concessão compatível com os direitos do autor ou autorização bind para o Role referenciado. No laboratório, o operador cria um Role de leitura que já possui, mas não consegue criar um Role que lê Secrets. Depois consegue delegar um Role especificamente autorizado por bind, sem receber autorização para associar qualquer outro Role. Estas exceções precisam de uma decisão própria de governação. Uma pipeline que pode associar um Role poderoso a qualquer subject pode também concedê-lo a uma identidade que controla. Limitar o nome do Role não é o mesmo que limitar os destinatários. Se o requisito inclui destinatários aprovados, define e valida os controlos adicionais necessários. Na revisão, separa quatro perguntas: quem cria o objeto, que Role pode escolher, a quem pode concedê-lo e como a concessão será retirada. Conserva a lista dos bindings produzidos pela automação; retirar direitos à pipeline não apaga os objetos que ela já criou.
Tratar credenciais como um ciclo de vida
Seleciona uma ServiceAccount dedicada quando a aplicação precisa de uma identidade própria. Criar a conta não altera o template de um Deployment. Confirma serviceAccountName no template e no Pod efetivo, incluindo a nova instância após uma mudança. Para aplicações sem necessidade de acesso à API, reduz a exposição a credenciais automáticas. O valor explícito de automountServiceAccountToken no Pod prevalece sobre o da conta. Esta configuração controla a montagem automática, não revoga todos os tokens já emitidos nem impede por si só um volume projetado explicitamente configurado. Um token tem destinatários e condições de validade. No laboratório, um token para uma audience diferente recebe 401 mesmo depois de o operador ter direitos de leitura. Acrescentar um Role não corrige esse problema. Numa aplicação que usa tokens projetados, verifica também a capacidade de voltar a ler a credencial após a rotação. Um processo que guarda indefinidamente a cópia inicial pode falhar enquanto uma nova instância funciona. O teste de aceitação deve reproduzir o comportamento do cliente e guardar resultados, não o token. Não uses a eliminação de uma ServiceAccount partilhada como primeira resposta a um único acesso excessivo; considera o impacto em todos os workloads que a utilizam.
Procurar caminhos indiretos de acesso
A matriz de pedidos diretos é necessária, mas não descreve todo o poder de uma identidade. Poder criar workloads pode permitir montar recursos ou selecionar contas no mesmo namespace, dependendo dos controlos existentes. Poder emitir tokens de outras ServiceAccounts pode permitir atuar com os direitos dessas contas. Estes caminhos devem entrar na análise antes de concluir que uma recusa de get secrets constitui isolamento. Separa o que RBAC controla dos campos e comportamentos que exigem admissão, separação de namespaces ou outro mecanismo. Confirma o controlo implementado, em vez de o assumir a partir do nome de um perfil. Revê também permissões que parecem inofensivas pelo verbo. Get em nodes/proxy não deve ser aprovado como simples leitura do objeto Node: o percurso para o kubelet tem capacidades distintas. Uma avaliação semelhante aplica-se à impersonação. Um teste --as pode ajudar a avaliar autorização para uma identidade representada, mas não demonstra que a credencial real da aplicação é aceite. Para fechar um incidente de 401, precisas de evidência dessa credencial e do destinatário, sem a expor. Estes casos mostram porque o responsável técnico deve rever o efeito operacional do acesso, além da sintaxe do Role.
Exercício guiado: prever, executar e explicar
Antes de executar o código abaixo, prepara uma tabela com três colunas: pedido, resultado previsto e motivo. Usa um cluster kind descartável com nome dr-cks-identity- seguido de um identificador único, kubeconfig privado e servidor em loopback. O script recusa outro padrão de nome e uma versão de servidor diferente da observada. Precisa de Python e de um caminho explícito para kubectl 1.37.1. A execução registada utilizou kind 0.33.0, Kubernetes 1.37.0 e cliente 1.37.1; o exame consultado indica 1.35. O ensaio demonstra os comportamentos observados nessa versão local, sem certificar equivalência integral ao exame. Executa python3 run.py --kubectl /caminho/kubectl --kubeconfig /caminho/privado/config --cluster dr-cks-identity-IDENTIFICADOR --output /caminho/novo/evidence.json. O script cria um namespace aleatório, duas contas, dois ConfigMaps e Roles locais. Os pedidos avaliados usam tokens reais por HTTPS, com validação do certificado, sem impersonação. A credencial administrativa serve apenas para preparar e retirar os objetos. Prevê primeiro quais as criações aceites, que leituras falham e o que acontece quando uma concessão é retirada. Compara cada previsão com as 19 observações do relatório. Se houver divergência, explica o atributo do pedido ou o caminho de concessão responsável antes de alterar uma regra.
Interpretar resultados e fechar a prática
No percurso registado, o primeiro pedido recebe 403; a concessão por nome permite 200 no objeto aprovado, conservando 403 no outro objeto. A listagem filtrada funciona e a não filtrada falha. O token com outra audience recebe 401. A criação de um Role com direitos já detidos recebe 201; a tentativa de incluir leitura de Secrets recebe 403. O binding permitido concede acesso à segunda conta. Quando retiras direitos ao delegador, a segunda conta mantém a concessão existente; só a remoção do binding correspondente retira essa leitura. Por fim, eliminar a conta representada torna o seu token inaceitável no pedido observado. O relatório guarda códigos, nomes dos controlos e hash do script. Não guarda tokens ou respostas contendo credenciais. O script remove o namespace e o diretório privado; o responsável pelo ensaio deve eliminar também o cluster descartável e o kubeconfig administrativo. Confirma a limpeza antes de arquivar o resultado. Esta prática não executa um workload, uma política de admissão, SSO, NetworkPolicy ou uma simulação de exame. Não demonstra rotação de um token projetado dentro de um Pod. Esses limites orientam o próximo exercício e impedem que um relatório de autorização seja usado como prova de controlos não observados.
Preparar a entrega e a revisão de mudanças
Entrega a RUN a matriz aprovada, os manifestos, os responsáveis e o procedimento de retirada. Inclui um pedido permitido e pelo menos um pedido proibido, executados com a identidade correta. Conserva timestamp, versão, namespace, nome do recurso e resultado sanitizado. Num acesso temporário de fornecedor, define a evidência necessária para fechar o ticket: remover a delegação futura não basta se continuam a existir bindings para o fornecedor. Se o ensaio não puder ser feito, regista essa ausência e mantém a validação pendente, em vez de declarar retirada comprovada. Acrescenta RBAC à revisão de upgrades e de extensões da API. Wildcards podem abranger novos recursos; Roles agregados podem receber regras de objetos selecionados por labels. Uma alteração manual no resultado agregado pode ser reposta pelo controlador. Rever apenas o manifesto antigo deixa estas dependências por observar. A mesma atenção aplica-se à substituição de roleRef: o campo é imutável, pelo que a mudança precisa de substituir o binding e rever os subjects. Resume a decisão em linguagem útil para a operação: que tarefa pode ser feita, por quem, durante quanto tempo e que resultado prova que o acesso terminou. Os exemplos desta aula são fictícios e não representam procedimentos internos da BNP Paribas.
"""Original disposable-cluster RBAC delegation lab; only synthetic namespaced data."""
import argparse,base64,datetime,hashlib,json,os,shutil,ssl,subprocess,tempfile,time,uuid,urllib.request,urllib.error
from pathlib import Path
p=argparse.ArgumentParser();p.add_argument('--kubectl',required=True);p.add_argument('--kubeconfig',required=True);p.add_argument('--cluster',required=True);p.add_argument('--output',required=True);a=p.parse_args()
assert a.cluster.startswith('dr-cks-identity-');assert Path(a.kubeconfig).is_absolute();out=Path(a.output);assert not out.exists()
tmp=Path(tempfile.mkdtemp(prefix='dr-cks-identity-private-'));ns='dr-identity-'+uuid.uuid4().hex[:8];created=[];records=[];tokens={}
base=[a.kubectl,'--kubeconfig',a.kubeconfig,'--context','kind-'+a.cluster,'--cache-dir',str(tmp/'cache'),'--request-timeout=15s']
def k(*args,obj=None):
r=subprocess.run(base+list(args),input=json.dumps(obj)if obj is not None else None,text=True,capture_output=True,timeout=30)
if r.returncode:raise RuntimeError('Admin operation failed: '+str(args[:3])+': '+r.stderr[:300])
return r.stdout
def obj(kind,name,**fields):return dict(apiVersion='rbac.authorization.k8s.io/v1'if kind in ['Role','RoleBinding']else'v1',kind=kind,metadata=dict(name=name),**fields)
def role(name,rules):return obj('Role',name,rules=rules)
def rule(resources,verbs,names=None,group=''):
d=dict(apiGroups=[group],resources=resources,verbs=verbs)
if names is not None:d['resourceNames']=names
return d
def binding(name,ref,subject='operator'):return obj('RoleBinding',name,roleRef=dict(apiGroup='rbac.authorization.k8s.io',kind='Role',name=ref),subjects=[dict(kind='ServiceAccount',name=subject,namespace=ns)])
def create(x):return k('-n',ns,'create','-f','-',obj=x)
def request(who,method,path,body=None):
req=urllib.request.Request(server+path,data=json.dumps(body).encode()if body is not None else None,headers={'Authorization':'Bearer '+tokens[who],'Content-Type':'application/json'},method=method)
try:
with urllib.request.urlopen(req,context=ctx,timeout=15)as res:return res.status,json.load(res)
except urllib.error.HTTPError as err:return err.code,json.load(err)
def check(label,who,method,path,expected,body=None):
status,result=request(who,method,path,body);assert status==expected,(label,status,result.get('reason'));records.append(dict(name=label,status=status,expected=expected,passed=True));print(label,status,flush=True);return result
def settle(who,path,expected):
end=time.monotonic()+15
while time.monotonic()<end:
if request(who,'GET',path)[0]==expected:return
time.sleep(.2)
raise AssertionError('Authorization propagation deadline exceeded')
try:
cfg=json.loads(k('config','view','--minify','--raw','-o','json'));cluster=cfg['clusters'][0]['cluster'];server=cluster['server'];assert server.startswith('https://127.0.0.1:');assert not cluster.get('insecure-skip-tls-verify')
ctx=ssl.create_default_context(cadata=base64.b64decode(cluster['certificate-authority-data']).decode());cfg=None
version=json.loads(k('version','-o','json'));assert version['serverVersion']['gitVersion']=='v1.37.0';assert version['clientVersion']['gitVersion']=='v1.37.1'
k('create','namespace',ns);created.append(ns)
for name in ['operator','recipient']:create(obj('ServiceAccount',name,automountServiceAccountToken=False));tokens[name]=k('-n',ns,'create','token',name,'--duration=10m').strip()
tokens['wrong-audience']=k('-n',ns,'create','token','operator','--audience=dr-internal-evidence','--duration=10m').strip()
for name in ['closing-status','other-status']:create(obj('ConfigMap',name,data=dict(state='synthetic')))
core='/api/v1/namespaces/'+ns;rbac='/apis/rbac.authorization.k8s.io/v1/namespaces/'+ns;target=core+'/configmaps/closing-status'
check('before-grant','operator','GET',target,403)
create(role('status-reader',[rule(['configmaps'],['get','list'],['closing-status'])]));create(binding('read-one','status-reader'));settle('operator',target,200)
check('named-get','operator','GET',target,200);check('other-name-denied','operator','GET',core+'/configmaps/other-status',403)
check('unfiltered-list-denied','operator','GET',core+'/configmaps',403)
listed=check('name-filtered-list','operator','GET',core+'/configmaps?fieldSelector=metadata.name%3Dclosing-status',200);assert [x['metadata']['name']for x in listed['items']]==['closing-status']
check('wrong-audience-rejected','wrong-audience','GET',target,401)
check('secret-list-denied','operator','GET',core+'/secrets',403)
create(role('author',[rule(['roles','rolebindings'],['create'],group='rbac.authorization.k8s.io')]));create(binding('author','author'))
check('role-create-with-held-rights','operator','POST',rbac+'/roles',201,role('derived-reader',[rule(['configmaps'],['get'],['closing-status'])]))
check('role-create-escalation-denied','operator','POST',rbac+'/roles',403,role('unheld-secrets',[rule(['secrets'],['get'])]))
create(role('secret-reader',[rule(['secrets'],['get'])]))
check('binding-unheld-role-denied','operator','POST',rbac+'/rolebindings',403,binding('secret-delegation','secret-reader','recipient'))
check('binding-held-role-allowed','operator','POST',rbac+'/rolebindings',201,binding('delegated','derived-reader','recipient'));settle('recipient',target,200)
check('recipient-get','recipient','GET',target,200)
create(role('status-updater',[rule(['configmaps'],['patch'],['closing-status'])]))
create(role('specific-delegator',[rule(['roles'],['bind'],['status-updater'],group='rbac.authorization.k8s.io')]));create(binding('specific-delegator','specific-delegator'))
check('specific-bind-allowed','operator','POST',rbac+'/rolebindings',201,binding('patch-delegation','status-updater','recipient'))
check('unlisted-bind-still-denied','operator','POST',rbac+'/rolebindings',403,binding('second-secret-delegation','secret-reader','recipient'))
create(binding('read-two','status-reader'));k('-n',ns,'delete','rolebinding','read-one');check('second-binding-retains-access','operator','GET',target,200)
k('-n',ns,'delete','rolebinding','read-two');settle('operator',target,403);check('last-read-binding-removed','operator','GET',target,403)
check('recipient-grant-survives-delegator-revocation','recipient','GET',target,200)
k('-n',ns,'delete','rolebinding','delegated');settle('recipient',target,403);check('recipient-read-revoked','recipient','GET',target,403)
k('-n',ns,'delete','serviceaccount','operator');settle('operator',target,401);check('deleted-serviceaccount-token-rejected','operator','GET',target,401)
assert len(records)==19,len(records)
finally:
tokens.clear()
for space in created:k('delete','namespace',space,'--wait=true','--timeout=60s')
shutil.rmtree(tmp)
report=dict(executedAt=datetime.datetime.now(datetime.timezone.utc).isoformat(),scriptSha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),version=version,examVersion='1.35',versionDifferenceExplicit=True,observations=records,cleanup=dict(namespacesRemoved=len(created),temporaryMaterialRemoved=not tmp.exists(),tokensRecorded=False),scope='Actual HTTPS API requests with ServiceAccount tokens on one disposable Kubernetes1.37.0 kind node; no impersonation, production credentials, workload execution, SSO, admission-policy validation, NetworkPolicy or certification simulation.')
out.write_text(json.dumps(report,indent=2)+'\n');print('PASS',len(records))
O fornecedor mantém leitura após a pipeline perder create rolebindings: a concessão já criada exige retirada própria.
Armadilhas comuns
Tratar um Role vazio como recusa; confundir namespace da conta e do binding; confiar em --as para validar tokens; esquecer concessões já delegadas.
Tópicos relacionados: Auditoria de acesso · Admissão de workloads · Rotação de credenciais
Uma autorização correta liga identidade, operação e âmbito; a retirada só está comprovada quando os caminhos de concessão e os pedidos reais foram revistos.
Referência: CKS certification and domains · Kubernetes v1.35; current six-domain CKS outline