Preparar uma mudança que possa ser recuperada
Uma plataforma fictícia de pagamentos vai substituir a cadeia TLS antes do fecho diário. A alteração parece pequena: publicar um Secret e confirmar que o endpoint responde. Contudo, há consumidores com bundles diferentes, duas réplicas do controlador e sessões persistentes. A equipa de APS precisa de saber qual combinação de configuração e confiança está realmente ativa em cada caminho. Um único resultado verde pode representar apenas o consumidor mais atualizado. Define antes da janela o estado de partida, o destino autorizado e o critério para regressar. Guarda versões de manifests, referências de Secrets, certificados públicos, hashes dos artefactos e procedimentos de recuperação. Protege chaves privadas no mecanismo aprovado, sem as incluir no ticket. Associa responsáveis a distribuição de confiança, promoção do servidor, validação do cliente e decisão de continuidade. O rollback é uma combinação de dependências. Um certificado antigo pode estar dentro da validade e já não ter uma cadeia confiável para clientes novos. Uma configuração anterior pode depender de um binário retirado por risco conhecido. Estabelece o que tem de continuar disponível e autorizado durante a janela. Os exemplos desta aula são originais e fictícios, sem representar procedimentos internos da BNP Paribas. O objetivo é uma decisão operacional fundamentada em resultados observados.
Validar políticas pelo tráfego que deve mudar
Constrói uma matriz pequena de origem, destino, porta, protocolo e resultado esperado. Inclui uma ligação necessária e uma ligação semelhante que deve ser recusada. Se origem e destino estiverem isolados nas direções aplicáveis, verifica ambos. Corrigir apenas ingress na base de dados não prova que egress no frontend permite iniciar a sessão. Mantém DNS e disponibilidade como hipóteses separadas durante o diagnóstico. Uma NetworkPolicy aceite pela API não demonstra suporte de todas as funcionalidades pelo plugin. No caso de endPort, a implementação precisa de suportar intervalos; testa o início, um ponto intermédio e o limite, além de uma porta fora do âmbito. A prática TLS desta aula não executa CNI nem políticas em cluster. Os cenários de rede avaliam a leitura da documentação e o desenho de evidências necessárias, sem apresentar essas decisões como resultados de dataplane medidos. Depois de retirar uma permissão, uma ligação nova pode falhar e uma sessão antiga continuar. O efeito sobre conexões existentes depende da implementação. Se o requisito exigir terminar sessões abertas, define uma medida própria e avalia o impacto. No rollback, apagar a única política que isolava uma direção pode abrir acesso. Recuperar conectividade e recuperar a fronteira de segurança são critérios que precisam de ser verificados explicitamente.
Controlar seleção, benchmark e exposição dos nodes
Uma correção de YAML pode alterar quem corresponde a uma permissão. Um namespaceSelector e um podSelector no mesmo elemento exigem a combinação; elementos separados representam alternativas. Rever apenas os valores das labels sem ler a estrutura pode alargar o acesso. Confirma também quem pode mudar essas labels. A política pode permanecer intacta enquanto uma equipa torna o seu namespace elegível para um destino sensível. Uma mudança de plataforma exige rever o benchmark e o âmbito de avaliação. A página pública CIS distingue Kubernetes e variantes de serviços geridos. Usa o perfil aplicável, a versão e as responsabilidades concretas; não reutilizes um relatório antigo apenas porque a aplicação é a mesma. Esta aula não reproduz recomendações numeradas de PDFs CIS nem afirma ter executado uma avaliação CIS completa. Os exercícios usam decisões próprias de remediação e aceitação de risco. Quando um novo grupo de nodes usa outro template, confirma os caminhos para metadata cloud e endpoints de administração. Um teste administrativo bem-sucedido ao kubelet não prova recusa de acesso anónimo. Um node Ready não prova que carregou a correção de hardening. Liga a evidência ao processo efetivo e repete os testes relevantes depois da alteração, incluindo a condição que deve ser recusada e o acesso operacional que deve continuar.
Distribuir confiança antes de trocar o servidor
Na rotação didática, o serviço antigo usa uma raiz e o novo usa outra raiz com CA intermédia. O nome DNS do serviço permanece igual. Um cliente que conhece apenas a raiz antiga não aceita a cadeia nova. Antes da promoção, distribui uma sobreposição de confiança autorizada e confirma que os consumidores relevantes conseguem estabelecer novas ligações com ambos os caminhos previstos. Sobreposição tem duração e âmbito. Acrescentar uma raiz amplia o conjunto de emissores aceites, pelo que deve corresponder à mudança aprovada. Depois de migrar, só retira a raiz antiga quando o inventário e os testes demonstrarem que já não é necessária, incluindo a estratégia de recuperação. Não copies a chave privada do servidor para os clientes: estes precisam do material público de confiança adequado ao seu papel. O laboratório usa SSLContext explícitos, sem carregar o trust store do sistema nem desativar verificação do nome. Mostra quatro relações: confiança antiga aceita servidor antigo, confiança nova recusa servidor antigo, confiança antiga recusa servidor novo e sobreposição aceita ambos. O resultado é específico dos certificados e contextos criados. Não demonstra revogação, mTLS, distribuição empresarial de bundles ou comportamento de qualquer controlador Ingress. Esses elementos exigem validação própria no ambiente onde serão usados.
Entregar a cadeia e validar o candidato
Um leaf correto não basta quando o cliente não consegue construir o caminho até à raiz em que confia. A prática arranca um endpoint que entrega apenas o leaf novo e outro que entrega leaf e intermédio. O cliente tem apenas a raiz nova no seu store. O primeiro é recusado e o segundo passa. O intermédio fornecido ajuda a construir o caminho; não é uma autorização para confiar arbitrariamente em qualquer CA enviada pelo servidor. Prepara um contexto candidato com cadeia e chave correspondentes antes de alterar o contexto ativo. No ensaio, tentar carregar a chave antiga com a cadeia nova falha. O serviço que já estava válido continua a responder porque o candidato recusado nunca foi promovido. Esta separação torna a falha local observável sem a introduzir deliberadamente no tráfego dos consumidores. Um Secret atualizado continua a precisar de adoção pelo controlador. Tempos de observação e reload variam por implementação. Se dois caminhos devolverem fingerprints diferentes, correlaciona réplicas, configuração e material realmente servido. Não concluas automaticamente que a CA assinou mal. Valida nome, cadeia e resultado da ligação a partir do caminho usado pelos consumidores. Conserva o estado anterior autorizado até cumprires os critérios de promoção e recuperação, sem deixar chaves privadas em logs ou relatórios.
Separar ficheiros, contextos e sessões
O exercício mantém um cliente com confiança antiga já carregada. Depois altera o ficheiro para incluir ambas as raízes. Aquele contexto continua a recusar o servidor novo; um contexto construído com o ficheiro atualizado passa. O mesmo padrão aparece no servidor: substituir os ficheiros em disco não muda o certificado que um contexto já carregado apresenta a uma ligação nova. A prática promove um contexto novo explicitamente. Uma sessão aberta antes da promoção continua a trocar mensagens e conserva o fingerprint observado no handshake antigo. Entretanto, uma nova ligação recebe o certificado novo. Ambos os resultados podem ser verdadeiros. Para validar a rotação, distingue disponibilidade das sessões existentes, aceitação de novos handshakes e eventual drenagem exigida pelo processo. O ensaio não reutiliza sessões TLS nos probes e regista sessionReused para evitar confundir a medição. Quando o bundle em disco passa a conter apenas a raiz nova, um contexto anterior com sobreposição ainda aceita o servidor antigo. Um contexto novo já o recusa. Retirar confiança num ficheiro não prova retirada de todas as instâncias em memória. Inventaria consumidores, mecanismos de reload e sessões relevantes. Não generalizes estes detalhes da biblioteca Python para todos os runtimes; usa-os para formular os testes que o componente real precisa de passar.
Executar o laboratório de rotação
Pré-requisitos: Python 3.13 ou posterior e OpenSSL 3.x, com permissão para sockets locais. Guarda o código como run.py e executa python3 run.py --output resultado-novo.json. Usa --openssl /caminho/openssl se houver vários executáveis. O script recusa sobrescrever o resultado, cria uma pasta temporária privada e gera as suas próprias raízes, chaves e certificados. O hostname payments.training.test é passado à verificação TLS, mas a ligação TCP usa apenas loopback. Lê as 16 observações e relaciona cada uma com a hipótese testada. Compara aceitação e recusa com confiança antiga, nova e sobreposta; cadeia incompleta e completa; ficheiros alterados com contextos antigos e novos; sessão persistente e handshake novo; candidato com chave errada e contexto ativo preservado. Os fingerprints e portas variam entre execuções porque o material é descartável. Compara relações e predicados, sem exigir identificadores iguais. No fim, confirma temporaryKeysRemoved e serversStopped. Os ensaios finais usam Python 3.13.1 e OpenSSL 3.6.5, conforme os relatórios. Não há cluster Kubernetes nem controlador Ingress, CNI, DNS público, trust store de produção ou serviço de revogação nesta prática. A página oficial CKS continua a indicar runtime 1.35 e exame prático de 120 minutos; este exercício local não é uma reprodução desse exame.
Decidir o fecho e conservar recuperação autorizada
No caso final, faltam 20 minutos para o fecho. Clientes atualizados ligam ao servidor novo, mas consumidores antigos falham em novos handshakes. As sessões que já estavam abertas continuam a funcionar. A decisão não deve usar essa continuidade como prova de aceitação universal. Se a distribuição e validação dos consumidores não couberem na janela, aplica o rollback autorizado e confirma novamente material servido, confiança carregada e caminhos relevantes. Mantém os artefactos de recuperação como conjuntos verificáveis. Um binário transferido para uma zona isolada precisa da assinatura e material correspondentes, sem misturar versões com o mesmo nome de ficheiro. Uma assinatura válida também não torna aprovada uma versão retirada por exposição conhecida. A equipa precisa de um destino autorizado ou de uma exceção explícita, com impacto e responsabilidade documentados. A prática TLS não executa verificação de binários Kubernetes; os cenários distinguem esse controlo da validação criptográfica de certificados. Resumo: valida o tráfego pretendido, a configuração efetiva, a distribuição de confiança e novas ligações. Preserva a relação entre recuperação técnica e autorização atual. Liga esta aula a NetworkPolicy, identidades, gestão de Secrets, cadeia de fornecimento e mudança operacional. O aprofundamento CKS continua com resposta nas restantes áreas, simulados práticos completos e revisão especializada independente.
#!/usr/bin/env python3
"""Original TLS trust-rollover lab. Python3.13+ and OpenSSL3.x, loopback only.
Creates disposable CAs/keys; no system trust, Kubernetes controller, CNI,
production endpoint, revocation service or full practical mock is exercised.
"""
import argparse, datetime, hashlib, json, platform, shutil, socket, ssl
import subprocess, tempfile, threading
from pathlib import Path
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--openssl',default='openssl')
p.add_argument('--output',required=True)
a=p.parse_args();out=Path(a.output).resolve()
if out.exists():raise SystemExit('Choose a new output path; existing reports are not overwritten.')
HOST='payments.training.test';observations=[];servers=[];persistent=None
def cmd(root,*args):
r=subprocess.run([a.openssl,*args],cwd=root,text=True,capture_output=True,timeout=20)
if r.returncode:raise RuntimeError('OpenSSL '+args[0]+' failed: '+r.stderr[-700:])
return r.stdout.strip()
def observe(name,passed,**data):
observations.append(dict(name=name,passed=bool(passed),observed=data))
if not passed:raise AssertionError(name)
def client(bundle):
c=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
c.minimum_version=ssl.TLSVersion.TLSv1_2
c.verify_flags|=ssl.VERIFY_X509_STRICT
c.load_verify_locations(cafile=str(bundle))
return c
def server_context(chain,key):
c=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
c.minimum_version=ssl.TLSVersion.TLSv1_2
c.load_cert_chain(str(chain),str(key))
return c
class Server:
def __init__(self,context):
self.context=context;self.stop=threading.Event();self.workers=[]
self.sock=socket.socket();self.sock.bind(('127.0.0.1',0));self.sock.listen(20)
self.sock.settimeout(.1);self.port=self.sock.getsockname()[1]
self.thread=threading.Thread(target=self.accept,daemon=True);self.thread.start();servers.append(self)
def accept(self):
while not self.stop.is_set():
try:raw,_=self.sock.accept()
except socket.timeout:continue
except OSError:break
t=threading.Thread(target=self.serve,args=(raw,self.context),daemon=True);self.workers.append(t);t.start()
def serve(self,raw,context):
raw.settimeout(8)
try:
with context.wrap_socket(raw,server_side=True)as conn:
while True:
data=conn.recv(64)
if not data:break
conn.sendall(data)
except (ssl.SSLError,OSError):pass
finally:raw.close()
def close(self):
self.stop.set();self.sock.close();self.thread.join(2)
for t in self.workers:t.join(9)
return not self.thread.is_alive() and not any(t.is_alive()for t in self.workers)
def connect(c,server):
raw=socket.create_connection(('127.0.0.1',server.port),timeout=3)
try:return c.wrap_socket(raw,server_hostname=HOST)
except Exception:raw.close();raise
def probe(c,server):
try:
with connect(c,server)as conn:
conn.sendall(b'check');data=conn.recv(64)
return dict(accepted=data==b'check',leafSha256=hashlib.sha256(conn.getpeercert(binary_form=True)).hexdigest(),protocol=conn.version(),sessionReused=conn.session_reused)
except ssl.SSLCertVerificationError as e:
return dict(accepted=False,verifyCode=e.verify_code,verifyMessage=e.verify_message)
def check(name,c,server,expected,fingerprint=None):
r=probe(c,server);observe(name,r['accepted']==expected and (fingerprint is None or r.get('leafSha256')==fingerprint),**r);return r
report=dict(startedAt=datetime.datetime.now(datetime.timezone.utc).isoformat(),scriptSha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),versions=dict(python=platform.python_version(),pythonTLS=ssl.OPENSSL_VERSION,openssl=cmd(None,'version')),observations=observations,fullPracticalMock=False,independentVerification=False,scope='Actual local TLS handshakes, chain delivery, CA overlap, loaded contexts, live connection and candidate-context rejection. No Kubernetes ingress controller, CNI, public DNS, mTLS, revocation or production trust store.')
temp=tempfile.TemporaryDirectory(prefix='dr-cks-rollover-');root=Path(temp.name)
try:
root.chmod(0o700)
def key(name):
cmd(root,'genpkey','-algorithm','RSA','-pkeyopt','rsa_keygen_bits:2048','-out',name+'.key');(root/(name+'.key')).chmod(0o600)
def ca(name):
key(name);cmd(root,'req','-new','-x509','-key',name+'.key','-out',name+'.pem','-days','2','-subj','/CN='+name,'-addext','basicConstraints=critical,CA:TRUE','-addext','keyUsage=critical,keyCertSign,cRLSign','-addext','subjectKeyIdentifier=hash')
def issue(name,issuer,is_ca=False):
key(name);cmd(root,'req','-new','-key',name+'.key','-out',name+'.csr','-subj','/CN='+name)
ext='basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\n'if is_ca else 'basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=DNS:'+HOST+'\n'
(root/(name+'.ext')).write_text(ext+'subjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid,issuer\n')
cmd(root,'x509','-req','-in',name+'.csr','-CA',issuer+'.pem','-CAkey',issuer+'.key','-CAcreateserial','-out',name+'.pem','-days','1','-extfile',name+'.ext')
ca('old-root');ca('new-root');issue('old-leaf','old-root');issue('new-intermediate','new-root',True);issue('new-leaf','new-intermediate')
old=root/'old-leaf.pem';new=root/'new-leaf.pem';chain=root/'new-chain.pem';chain.write_bytes(new.read_bytes()+(root/'new-intermediate.pem').read_bytes())
bundle=root/'trust.pem';bundle.write_bytes((root/'old-root.pem').read_bytes())
old_client=client(bundle);new_client=client(root/'new-root.pem')
active_cert=root/'active-chain.pem';active_key=root/'active.key';shutil.copy2(old,active_cert);shutil.copy2(root/'old-leaf.key',active_key)
serving=Server(server_context(active_cert,active_key));old_server=Server(server_context(old,root/'old-leaf.key'));new_server=Server(server_context(chain,root/'new-leaf.key'));missing=Server(server_context(new,root/'new-leaf.key'))
old_fp=check('initial-old-trust-accepts-old-server',old_client,serving,True)['leafSha256']
check('new-trust-denies-old-server',new_client,old_server,False)
check('old-trust-denies-new-server',old_client,new_server,False)
bundle.write_bytes((root/'old-root.pem').read_bytes()+(root/'new-root.pem').read_bytes());dual=client(bundle)
check('overlap-trust-accepts-old-server',dual,old_server,True,old_fp)
new_fp=check('overlap-trust-accepts-new-server',dual,new_server,True)['leafSha256']
check('missing-intermediate-denied',new_client,missing,False)
check('full-chain-accepted',new_client,new_server,True,new_fp)
check('changed-trust-file-does-not-refresh-old-context',old_client,new_server,False)
check('new-context-loads-updated-trust-file',client(bundle),new_server,True,new_fp)
persistent=connect(old_client,serving);persistent.sendall(b'before');assert persistent.recv(64)==b'before'
shutil.copy2(chain,active_cert);shutil.copy2(root/'new-leaf.key',active_key)
check('changed-server-files-do-not-refresh-context',old_client,serving,True,old_fp)
serving.context=server_context(active_cert,active_key)
check('replacement-context-serves-new-leaf',dual,serving,True,new_fp)
persistent.sendall(b'after');reply=persistent.recv(64);fp=hashlib.sha256(persistent.getpeercert(binary_form=True)).hexdigest()
observe('established-session-retains-old-handshake',reply==b'after'and fp==old_fp,echoContinues=reply==b'after',leafSha256=fp,oldLeafSha256=old_fp,newLeafSha256=new_fp)
bundle.write_bytes((root/'new-root.pem').read_bytes());retired=client(bundle)
check('retired-root-denied-by-fresh-context',retired,old_server,False)
check('loaded-overlap-context-still-trusts-old-root',dual,old_server,True,old_fp)
check('fresh-new-only-context-accepts-new-server',retired,serving,True,new_fp)
rejected=False
try:server_context(chain,root/'old-leaf.key')
except ssl.SSLError:rejected=True
r=probe(retired,serving)
observe('mismatched-candidate-rejected-active-service-preserved',rejected and r['accepted']and r['leafSha256']==new_fp,candidateRejected=rejected,activeService=r)
report['passed']=all(x['passed']for x in observations)
finally:
if persistent:persistent.close()
closed=[s.close()for s in servers];temp.cleanup()
report['cleanup']=dict(temporaryKeysRemoved=not root.exists(),serversStopped=all(closed),realCredentialsUsed=False,systemTrustModified=False)
report['finishedAt']=datetime.datetime.now(datetime.timezone.utc).isoformat();out.write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(dict(passed=report.get('passed',False),observations=len(observations),output=str(out))))
Sessões antigas continuam após a rotação, mas clientes sem a CA nova falham quando abrem uma ligação.
Armadilhas comuns
Confundir ficheiros com contextos carregados, sessões antigas com novos handshakes e assinatura válida com versão aprovada.
Tópicos relacionados: NetworkPolicy e exposição · Cadeias e confiança TLS · Gestão de mudança e rollback
Uma mudança recuperável exige evidência do estado efetivo e das dependências que ainda tornam o rollback utilizável.
Referência: CKS certification and domains · Kubernetes v1.35; current six-domain CKS outline