← CKS: segurança Kubernetes em produção
24 / 25 · 120 MIN

Encriptar, rodar chaves e recuperar Secrets

Observa dados no etcd, executa rotação faseada e recupera uma falha de leitura sem perder valores.

Definir o que está protegido

Uma aplicação de reconciliação usa uma password guardada num Secret. Há três perguntas diferentes: quem pode obter o valor pela API, como o armazenamento persistente representa esse valor e se a password ainda é aceite pela base de dados. Para investigar, associa cada pergunta ao seu controlo e à sua observação. Um administrador autorizado pode continuar a ler o conteúdo depois de ativar encriptação em repouso. Isso é necessário para utilização do recurso e não demonstra, por si só, uma falha do armazenamento. No laboratório, observa primeiro uma escrita com identity e depois uma com aescbc. Compara o prefixo e a presença do marcador sintético no etcd com a leitura do mesmo valor pela API. A representação base64 de data pode permanecer igual nas duas leituras. Aprende a explicar esta diferença ao responsável de projeto: uma captura de kubectl get não substitui evidência da camada persistente, e nenhuma das duas demonstra que uma credencial exposta deixou de funcionar no sistema externo. O exercício usa aescbc para tornar visíveis as fases de migração. A documentação Kubernetes 1.35 não o recomenda para novas escolhas de produção devido ao risco de padding oracle. Não copies esta seleção como arquitetura de produção. Avalia um provider apropriado, gestão e acesso às chaves, dependências de recuperação e o modelo de ameaça. A comparação operacional observada aqui não é uma avaliação criptográfica nem uma aprovação de desenho.

Preparar uma hipótese e uma configuração ordenada

Antes de executar, escreve uma previsão para cada estado. Com identity primeiro, espera novas escritas em claro. Com aescbc primeiro e key1 como primeira chave, espera novas escritas com key1. Um objeto anterior pode manter a representação antiga até ser reescrito. Se a previsão falhar, confirma a configuração adotada e o objeto observado antes de modificar mais componentes. Essa disciplina reduz mudanças contraditórias durante uma janela curta. A seleção de recursos também limita a conclusão. Proteger secrets não cobre automaticamente um ConfigMap no mesmo namespace. Ao trabalhar com wildcards, a ordem das entradas e as exclusões específicas precisam de revisão, incluindo combinações inválidas. A referência EncryptionConfiguration descreve essa estrutura. Desenha uma pequena matriz com recurso, regra aplicável, primeiro provider e leitor dos dados existentes. Usa-a para detetar uma exceção colocada depois de uma entrada mais abrangente ou uma hipótese errada de proteção por namespace. No caso fictício de fundos, a equipa encontra uma string de ligação sensível num ConfigMap. A decisão inclui rever onde o valor deve ser guardado e quem o pode ler, além do âmbito de encriptação. Não classifiques o controlo como totalmente eficaz por teres observado um único Secret. Regista também o que não foi ensaiado: o laboratório executa a seleção de Secrets; wildcards e exclusões são exercícios de análise da documentação nesta aula.

Executar o laboratório e registar as fronteiras

O código associado cria um cluster kind temporário com um único control plane, API em localhost e kubeconfig privado. Precisa de Python 3, Docker, kind 0.33.0, kubectl 1.35.8 e da imagem Kubernetes 1.35.8 fixada pelo digest presente no código. Os argumentos --kind, --kubectl e --report permitem indicar executáveis e destino do relatório. Não usa o contexto normal do operador. As chaves aleatórias e os quatro valores de Secret são exclusivos deste ensaio; os relatórios guardam observações e identificadores, sem divulgar o material das chaves. O ensaio observa seis recarregamentos automáticos e 32 verificações: escrita inicial, ativação, dados antigos não migrados, segunda chave ainda sem escrever, promoção, falha por retirada prematura, recuperação e retirada depois da migração. O watch cache está desativado para observar a falha de leitura do armazenamento sem uma resposta previamente desencriptada em cache. Esta escolha delimita o teste: não demonstra comportamento normal de cache, desempenho ou disponibilidade num control plane redundante. Antes de executar, prevê quais os Secrets legíveis em cada fase. Depois compara as previsões com o relatório e explica qualquer diferença. A eliminação final remove apenas o cluster e o material temporário criados pelo script. As duas execuções finais registadas passaram as mesmas 32 verificações. Nenhuma utilizou dados de produção. Este exercício não executa KMS, restauro de snapshot, rotação distribuída ou um simulado prático completo do CKS.

Migrar sem perder alterações concorrentes

O primeiro Secret do ensaio conserva o marcador legível no etcd depois de a encriptação passar a primeiro provider. A criação de outro Secret já produz um prefixo key1. Esta comparação mostra por que um teste de nova escrita não fecha o trabalho sobre dados anteriores. A reescrita do primeiro objeto transforma a representação e preserva o valor devolvido pela API. Regista separadamente essas duas observações: proteção de armazenamento e integridade do conteúdo. Num ambiente com escritores concorrentes, uma leitura seguida de PUT pode encontrar uma versão diferente do recurso. Um erro 409 Conflict exige voltar a obter o estado atual e reavaliar a escrita, preservando alterações legítimas. Não transformes o procedimento numa reposição de uma cópia antiga de todas as credenciais. A referência da API explica resourceVersion e a deteção de atualizações perdidas. O laboratório não introduz concorrência deliberada; o tratamento de conflitos é um exercício de decisão adicional. Define o âmbito antes de retirar compatibilidade. Se EncryptionConfiguration protege Secrets de todos os namespaces, migrar apenas funds deixa uma lacuna. O script reescreve todos os Secrets do seu cluster descartável, incluindo os gerados para o próprio cluster, e verifica o prefixo antes da fase final. Não generalizes essa autorização para clusters partilhados. Em operação, identifica responsáveis, impacto, controlo de alterações e evidência exigida para o conjunto realmente abrangido.

Separar introdução, promoção e retirada de chaves

Na fase de introdução, key2 fica depois de key1. O novo Secret staged continua a usar key1. Na promoção, key2 passa para a primeira posição e o Secret new2 usa essa chave. Entretanto, um objeto anteriormente gravado com key1 continua dependente do leitor antigo. Estas três observações permitem explicar a mudança sem afirmar que todos os dados mudaram ao mesmo tempo. Com recarregamento automático, a hora de modificação do ficheiro não é confirmação de adoção. O script espera que avance a métrica apiserver_encryption_config_controller_automatic_reload_last_timestamp_seconds e faz depois observações de leitura ou escrita. A espera pode aproximar-se de um minuto por fase. Define um limite e regista falhas em vez de continuar como se o reload tivesse ocorrido. No ensaio, seis fases tornam a execução deliberadamente mais demorada do que um teste de sintaxe. Num control plane com várias instâncias, prepara a capacidade de leitura em todas antes de promover novas escritas. Uma consulta através do balanceador não identifica necessariamente a instância ainda desatualizada. Constrói um plano por instância e um critério de progressão. A aula aplica a documentação a este problema, mas o cluster local tem apenas um API server. Num handover, descreve a topologia realmente observada para que outro operador não interprete uma prova local como ensaio de alta disponibilidade.

Recuperar de uma retirada prematura

O laboratório retira key1 enquanto ainda existe ciphertext que depende dela. A leitura do Secret antigo falha; um Secret key2 continua legível. O hash dos bytes antigos mantém-se. O incidente demonstra uma indisponibilidade de leitura, não o desaparecimento automático do objeto. Essa distinção altera a resposta: preservar dados recuperáveis e restabelecer o leitor pode ser melhor do que apagar e recriar recursos incompletos. A fase seguinte repõe key1 como leitor depois de key2. A leitura anterior volta a funcionar. Só depois o script reescreve o conjunto de Secrets e verifica a utilização de key2 antes de retirar key1 e identity. No relatório, relaciona a falha, a preservação do ciphertext e a recuperação. Não concluas que qualquer falha de Secret tem esta causa; o caso foi construído com um estado conhecido para isolar o comportamento. Num fecho de fundos fictício, a indisponibilidade pode atingir jobs com dependências diferentes. APS deve identificar os afetados, manter o estado de processamento e confirmar critérios de retoma com negócio e segurança. Um Secret legível não prova que todos os jobs podem ser repetidos sem duplicação. A alternativa temporária pode ser suspender o lote afetado com impacto comunicado. A sequência operacional deste caso é didática e não representa procedimentos internos da BNP Paribas.

Relacionar recuperação, KMS e exposição de credenciais

Um snapshot antigo pode conter dados ainda protegidos por uma chave retirada do estado corrente. A integridade do ficheiro não demonstra capacidade de leitura desse estado histórico. Como exercício de desenho, relaciona cada ponto de recuperação com a configuração e o acesso ao material necessário. Mudar apenas o nome de key2 para key1 não recria a chave anterior. O laboratório não restaura snapshots: esta dependência é analisada conceptualmente e precisa de um ensaio de recuperação próprio. KMS também exige distinguir saúde de cobertura. A documentação Kubernetes recomenda KMS v2 quando possível e descreve a integração e os seus checks. Um provider saudável não inventaria nem reescreve todos os dados antigos. Num piloto, pede evidência de adoção, de novas escritas, de migração e de recuperação, com os limites de cada observação. Uma indisponibilidade da dependência pode ter efeitos diferentes segundo o estado de caches e dados, que não foram ensaiados neste cluster aescbc. Finalmente, rodar a chave de armazenamento conservando o valor de uma password não revoga essa password. No caso fictício, coordena a alteração no sistema que a aceita, a adoção pelos consumidores e a verificação do valor anterior. Relaciona sessões existentes e continuidade com as equipas responsáveis. Não feches um incidente de credencial exposta apenas porque o prefixo do Secret passou de key1 para key2.

Entregar evidência utilizável pela equipa seguinte

Produz uma matriz com estado inicial, mudança, observação, interpretação e decisão seguinte. Inclui pelo menos cinco linhas: objeto antigo em claro após ativação, key2 introduzida sem ser escritora, key2 promovida com dados key1 remanescentes, falha de leitura após retirada e recuperação depois da reposição. Em cada linha, identifica se a observação vem da API, do armazenamento ou da configuração adotada. Evita transformar uma ausência de erro numa prova de todos os três. Para fechar o exercício, outra pessoa deve conseguir explicar por que a primeira retirada falhou e a segunda passou, sem receber valores secretos. Entrega versões, digest da imagem, hash do código, fases observadas, resultados e limpeza. Acrescenta os pontos pendentes: redundância, conflitos deliberados, cache normal, KMS e restauro de snapshot. As duas execuções automáticas aumentam a repetibilidade da observação; não constituem revisão especializada independente nem certificação. Resumo: define o âmbito, distingue escritor de leitores, confirma adoção, migra mantendo valores e só retira compatibilidade depois de observar o conjunto relevante. Quando a aplicação usa uma credencial exposta, trata também a credencial no sistema externo. Relaciona estes passos com RBAC, auditoria, recuperação de desastre, gestão de mudanças e resposta a incidentes. Usa as perguntas do módulo para justificar a próxima ação e explicar por que cada alternativa falha no contexto apresentado.

#!/usr/bin/env python3
"""Original disposable Kubernetes 1.35 encryption-at-rest exercise.
Creates and deletes its own kind cluster. Only generated keys and synthetic Secrets.
Requires Docker, kind 0.33 and kubectl 1.35.8. Never uses the default kubeconfig.
Usage: python3 run.py --kind /path/kind --kubectl /path/kubectl --report /path/new.json
"""
import argparse,base64,datetime,hashlib,json,os,pathlib,re,shutil,subprocess,tempfile,time,uuid
IMAGE='kindest/node:v1.35.8@sha256:07b2536e30b803ed61d1677a79df6115f798ce64c80f9e22f6ed45afd09323c0'
PREFIX=b'k8s:enc:aescbc:v1:'
def main():
 ap=argparse.ArgumentParser();ap.add_argument('--kind',required=True);ap.add_argument('--kubectl',required=True);ap.add_argument('--report',type=pathlib.Path,required=True);a=ap.parse_args();assert not a.report.exists()
 os.umask(0o077);work=pathlib.Path(tempfile.mkdtemp(prefix='dr-cks-encryption-'));enc=work/'enc';enc.mkdir();cfg=enc/'config.json';kubeconfig=work/'kubeconfig';name='dr-cks-encryption-'+uuid.uuid4().hex[:10];node=name+'-control-plane';ns='dr-encryption';records=[];phases=[];started=False
 keys={x:base64.b64encode(os.urandom(32)).decode() for x in ['key1','key2']};marker='DR_ORIGINAL_SYNTHETIC_VALUE_20261007'
 report=dict(startedAt=datetime.datetime.now(datetime.timezone.utc).isoformat(),scriptSha256=hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest(),nodeImage=IMAGE,cluster=name,observations=records,phases=phases,syntheticOnly=True,actualCluster=True,actualEtcdInspection=True,actualEncryptionRotation=True,HA=False,KMS=False,backupRestore=False,watchCache=False,fullPracticalMock=False,independentVerification=False)
 def run(cmd,data=None,timeout=25,check=True):
  r=subprocess.run(cmd,input=data,capture_output=True,text=True,timeout=timeout)
  if check and r.returncode:raise RuntimeError('Command failed ('+str(r.returncode)+'): '+str(cmd[:2])+' '+r.stderr[:160])
  return r
 def k(*args,obj=None,check=True):return run([a.kubectl,'--kubeconfig',str(kubeconfig),'--context','kind-'+name,'--cache-dir',str(work/'cache'),'--request-timeout=15s',*args],json.dumps(obj) if obj is not None else None,check=check)
 def observe(label,value,expected):
  records.append(dict(name=label,observed=value,expected=expected,passed=value==expected));print(label,flush=True);assert value==expected,(label,value,expected)
 def config(order,identity=False,identity_first=False):
  providers=[{'aescbc':{'keys':[{'name':n,'secret':keys[n]} for n in order]}}]
  if identity:providers=([{'identity':{}}]+providers) if identity_first else (providers+[{'identity':{}}])
  d=dict(apiVersion='apiserver.config.k8s.io/v1',kind='EncryptionConfiguration',resources=[{'resources':['secrets'],'providers':providers}]);tmp=enc/'pending.json';tmp.write_text(json.dumps(d));tmp.chmod(0o600);tmp.replace(cfg)
 def metric():
  text=k('get','--raw','/metrics').stdout;values=[float(line.rsplit(' ',1)[1]) for line in text.splitlines() if line.startswith('apiserver_encryption_config_controller_automatic_reload_last_timestamp_seconds')];return max(values,default=0)
 def wait(fn,timeout=150):
  end=time.monotonic()+timeout
  while time.monotonic()<end:
   try:
    x=fn()
    if x:return x
   except (RuntimeError,subprocess.TimeoutExpired):pass
   time.sleep(2)
  raise AssertionError('Timed out waiting for explicit observed state')
 def switch(label,order,identity=False):
  prior=metric();config(order,identity);began=time.monotonic();observed=wait(lambda:metric()>prior);phases.append(dict(name=label,keys=order,identityFallback=identity,reloadMetricAdvanced=observed,elapsedSeconds=round(time.monotonic()-began,2)));observe(label+'-reload-observed',observed,True)
 def secret(n):return dict(apiVersion='v1',kind='Secret',metadata={'name':n,'namespace':ns},stringData={'sample':marker+'_'+n})
 def create(n):k('create','-f','-',obj=secret(n))
 def get(n,check=True):return k('-n',ns,'get','secret',n,'-o','json',check=check)
 def value(n):return base64.b64decode(json.loads(get(n).stdout)['data']['sample']).decode()
 def raw(n,namespace=ns):
  r=k('-n','kube-system','exec','etcd-'+node,'--','etcdctl','--endpoints=https://127.0.0.1:2379','--cacert=/etc/kubernetes/pki/etcd/ca.crt','--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt','--key=/etc/kubernetes/pki/etcd/healthcheck-client.key','get','/registry/secrets/'+namespace+'/'+n,'-w','json')
  data=json.loads(r.stdout);assert data.get('count')==1;return base64.b64decode(data['kvs'][0]['value'])
 def stored(n,key):return raw(n).startswith(PREFIX+key.encode()+b':')
 def rewrite(n):k('replace','-f','-',obj=json.loads(get(n).stdout))
 try:
  config(['key1'],True,True)
  patch=dict(apiVersion='kubeadm.k8s.io/v1beta3',kind='ClusterConfiguration',apiServer=dict(extraArgs={'encryption-provider-config':'/etc/kubernetes/enc/config.json','encryption-provider-config-automatic-reload':'true','watch-cache':'false'},extraVolumes=[dict(name='dr-encryption',hostPath='/etc/kubernetes/enc',mountPath='/etc/kubernetes/enc',readOnly=True,pathType='Directory')]))
  kindcfg=dict(kind='Cluster',apiVersion='kind.x-k8s.io/v1alpha4',networking={'apiServerAddress':'127.0.0.1'},nodes=[dict(role='control-plane',extraMounts=[dict(hostPath=str(enc),containerPath='/etc/kubernetes/enc',readOnly=True)],kubeadmConfigPatches=[json.dumps(patch)])]);(work/'kind.json').write_text(json.dumps(kindcfg))
  started=True;print('creating-owned-cluster',name,flush=True)
  run([a.kind,'create','cluster','--name',name,'--image',IMAGE,'--config',str(work/'kind.json'),'--kubeconfig',str(kubeconfig),'--wait','120s'],timeout=240)
  report['versions']=json.loads(k('version','-o','json').stdout);observe('server-version',report['versions']['serverVersion']['gitVersion'],'v1.35.8');observe('client-version',report['versions']['clientVersion']['gitVersion'],'v1.35.8')
  observe('private-config-mode',oct(cfg.stat().st_mode & 0o777),'0o600');k('create','namespace',ns)
  create('legacy');observe('identity-stores-plaintext',(marker+'_legacy').encode() in raw('legacy'),True);observe('identity-api-readable',value('legacy'),marker+'_legacy')
  switch('enable-key1',['key1'],True);create('new1');observe('new-secret-key1',stored('new1','key1'),True);observe('new-secret-marker-hidden',marker.encode() in raw('new1'),False);observe('new-secret-api-readable',value('new1'),marker+'_new1');observe('legacy-not-auto-migrated',(marker+'_legacy').encode() in raw('legacy'),True)
  rewrite('legacy');observe('legacy-rewritten-key1',stored('legacy','key1'),True);observe('legacy-preserved-after-migration',value('legacy'),marker+'_legacy')
  switch('stage-key2',['key1','key2'],True);create('staged');observe('second-key-not-writer',stored('staged','key1'),True)
  switch('promote-key2',['key2','key1'],True);create('new2');observe('new-secret-key2',stored('new2','key2'),True);observe('old-ciphertext-remains-key1',stored('legacy','key1'),True);observe('old-key-still-decrypts',value('legacy'),marker+'_legacy')
  old_cipher=hashlib.sha256(raw('legacy')).hexdigest();switch('premature-retirement',['key2'],True)
  broken=get('legacy',False);observe('missing-key-read-fails',broken.returncode!=0,True);observe('missing-key-storage-error',any(x in broken.stderr for x in ['InternalError','unable to transform','no matching key']),True);observe('failed-read-preserves-ciphertext',hashlib.sha256(raw('legacy')).hexdigest(),old_cipher);observe('current-key-readable-during-failure',value('new2'),marker+'_new2')
  switch('restore-old-reader',['key2','key1'],True);observe('restored-key-recovers-read',value('legacy'),marker+'_legacy')
  inventory=json.loads(k('get','secrets','-A','-o','json').stdout)['items']
  for obj in inventory:k('replace','-f','-',obj=obj)
  report['rewrittenSecretCount']=len(inventory)
  observe('all-disposable-cluster-secrets-rewritten',all(raw(obj['metadata']['name'],obj['metadata']['namespace']).startswith(PREFIX+b'key2:') for obj in inventory),True)
  observe('all-exercise-secrets-rewritten',all(stored(n,'key2') for n in ['legacy','new1','staged','new2']),True)
  switch('retire-after-rewrite',['key2'],False)
  observe('cluster-secret-list-readable',len(json.loads(k('get','secrets','-A','-o','json').stdout)['items'])==len(inventory),True)
  observe('all-exercise-secrets-readable',all(value(n)==marker+'_'+n for n in ['legacy','new1','staged','new2']),True)
  observe('all-exercise-secrets-key2',all(stored(n,'key2') for n in ['legacy','new1','staged','new2']),True)
  observe('no-synthetic-marker-in-stored-ciphertext',all(marker.encode() not in raw(n) for n in ['legacy','new1','staged','new2']),True)
  report['scopeNote']='One disposable API server, watch cache explicitly disabled to observe storage read failures, local aescbc keys and four original synthetic Secrets. Final rewrite covers every Secret in this disposable cluster, including its generated bootstrap material. Six observed automatic reloads. No HA rollout,KMS,backup restoration,production secrets or real incident recovery.';report['passed']=True
 except BaseException as exc:report['passed']=False;report['error']=str(exc);raise
 finally:
  if started:run([a.kind,'delete','cluster','--name',name,'--kubeconfig',str(kubeconfig)],timeout=90,check=False)
  remaining=run(['docker','ps','-a','--filter','label=io.x-k8s.kind.cluster='+name,'--format','{{.Names}}'],check=False).stdout.strip();shutil.rmtree(work)
  report['cleanup']=dict(ownedClusterRemoved=not remaining,temporaryKeysRemoved=not work.exists(),dedicatedKubeconfigRemoved=not kubeconfig.exists(),realCredentialsUsed=False,defaultKubeconfigModified=False);report['finishedAt']=datetime.datetime.now(datetime.timezone.utc).isoformat();a.report.parent.mkdir(parents=True,exist_ok=True);a.report.write_text(json.dumps(report,indent=2)+'\n')
 print(json.dumps({'passed':report['passed'],'observations':len(records),'report':str(a.report)}))
if __name__=='__main__':main()
NA PRÁTICA

Retirar key1 antes de reescrever os objetos antigos impede a leitura; repor o leitor recupera os valores preservados.

Armadilhas comuns

Usar base64 como prova de confidencialidade, retirar leitores cedo demais ou confundir rotação do armazenamento com revogação de passwords.

Tópicos relacionados: RBAC e Secrets · Migração e concorrência · Recuperação de desastre · Gestão de mudanças

Leva esta ideia contigo

Uma nova escrita protegida não demonstra migração nem recuperação de todos os dados anteriores.

Criar conta

Referência: CKS domains and exam details · Kubernetes v1.35; current six-domain CKS outline

Kubernetes® e CKS são marcas comerciais ou marcas registadas de The Linux Foundation. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por The Linux Foundation. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.