← CKS: segurança Kubernetes em produção
12 / 14 · 120 MIN

Exposição do cluster e aceitação TLS

Diagnostica políticas de rede, valida identidade TLS e prepara evidência de segurança para RUN.

Definir a fronteira e a evidência de aceitação

Um projeto de segurança começa por identificar os percursos que pretende permitir e os que pretende impedir. Num serviço fictício de posições, o cliente de fecho precisa de chegar ao endpoint HTTPS; o processador precisa de DNS e da base de dados; o fornecedor precisa de diagnóstico limitado. Estes percursos têm identidades, protocolos e destinatários diferentes. Desenha cada ligação com origem, destino, porta, protocolo, nome esperado e responsável. Acrescenta o instante e a revisão de configuração usados no teste. Assim, um relatório não confunde o acesso do operador com o acesso da aplicação, nem um ensaio antigo com a release que vai entrar em produção. Transforma o requisito em duas observações: o comportamento necessário funciona e o comportamento proibido é recusado no âmbito definido. Uma recusa não prova automaticamente segurança; pode resultar de um serviço desligado. Uma ligação bem-sucedida também não prova autenticação correta. Neste módulo, separa declaração na API, aplicação do controlo, identidade do endpoint e resultado funcional. Essa separação permite atribuir a correção à equipa certa e evitar mudanças amplas antes de localizar a falha.

Ler a política sem perder a direção

Ao rever NetworkPolicy, começa pelos Pods selecionados e pela direção indicada em policyTypes. Não deduzas a direção a partir do nome da política. Uma lista de regras vazia e uma lista que contém uma regra vazia têm efeitos diferentes: ausência de autorizações nessa lista não é o mesmo que uma autorização sem condições. Escreve uma pequena matriz de testes antes de aplicar a mudança. Por exemplo, o processador pode precisar de UDP 53 para o resolver aprovado e TCP 5432 para a base, mas não de acesso a um endpoint de administração. Uma regra que omite protocol usa TCP; não a trates como autorização simultânea de UDP. Revê o conjunto de políticas aplicáveis e os dois sentidos de isolamento. Uma autorização ampla anterior pode continuar a permitir tráfego. No ensaio, confirma labels efetivos, namespaces, origem e destino, em vez de testar apenas o manifesto novo. Se a aplicação resolve nomes mas não alcança a base, a evidência já separa parte do percurso. Se nem o resolver responde, localiza esse passo antes de abrir todo o egress para recuperar o serviço.

Reconhecer limites da implementação

O objeto aceite pela API não é um recibo de aplicação simultânea em todos os nodes. O plugin processa a política e a transição pode produzir observações diferentes entre destinos. Regista a sequência e repete verificações com um prazo definido; uma recusa persistente continua a exigir diagnóstico. Não transformes a explicação de uma transição possível numa justificação ilimitada para qualquer falha. A equipa de RUN precisa de saber o que deve observar, quando escalar e qual é o critério de rollback. O percurso também pode alterar o endereço que a política observa. Num acesso por load balancer, a origem avaliada pode diferir do IP do cliente. Confirma a implementação antes de concluir que um ipBlock seleciona a população pretendida. Trata hostNetwork como uma condição própria: alguns plugins distinguem esse tráfego por Pod, enquanto outros o tratam como tráfego do node. Um teste num Pod normal não cobre automaticamente um agente com outra configuração. Documenta essas diferenças no plano, incluindo o plugin, a pool de nodes e os tipos de workload que o ensaio realmente representa.

Relacionar o Ingress com a identidade TLS

O Ingress declara encaminhamento; um controlador tem de o implementar. Confirma ingressClassName e a classe efetivamente atendida, o host da regra, o Secret referenciado e o endpoint ao qual o cliente chega. O formato TLS usa tls.crt e tls.key. A existência desses campos não prova que a chave corresponda ao certificado nem que o nome pedido esteja coberto. Na análise, distingue cadeia de confiança, validade temporal, utilização permitida e identidade do nome. Um certificado emitido por uma CA confiável para report.lab.test não autentica funds.lab.test quando só aquele nome consta do SAN. Numa rotação, comparar o Secret antes e depois é necessário mas insuficiente. Observa o certificado servido com o nome esperado no percurso real. Se há vários hosts ou controladores, conserva SNI e destino no registo do teste. A terminação TLS à entrada não demonstra cifragem do segmento seguinte; identifica separadamente o transporte até ao backend quando o requisito o exigir. Não uses a desativação da verificação do cliente como critério de aceitação. Ela pode esconder precisamente o erro de identidade que precisas de corrigir.

Executar o laboratório de aceitação TLS

O código abaixo requer Python 3.13 ou posterior e OpenSSL 3.x na linha de comandos. Guarda-o como run.py e executa python3 run.py. Cria duas CAs e um certificado de servidor temporários, com SAN funds.lab.test. Não instala confiança no sistema nem usa credenciais reais. Antes de executar, prevê os resultados de nome errado, CA errada, finalidade inadequada, IP ausente do SAN e instantes fora da validade. A opção -attime muda o instante usado pela verificação, sem alterar o relógio da máquina. A comparação de chaves públicas confirma um par correto e distingue uma chave de outra emissão, sem imprimir material privado. Depois, o programa abre ligações TLS reais em 127.0.0.1. Só a ligação com CA e nome corretos deve devolver a resposta sintética; os outros dois clientes devem falhar a verificação. O JSON reúne 12 observações e identifica as versões executadas. As chaves temporárias são removidas no fim. Explica a causa de cada recusa antes de ler os detalhes. Estes resultados demonstram os controlos TLS do exercício, sem executar DNS, um controlador Ingress, CNI ou verificação de revogação. A validação do cluster continua a precisar do seu próprio ensaio.

Avaliar metadata, endpoints e benchmarks

Um teste de metadata deve identificar a origem e a configuração usadas. Se o workload real está noutra pool ou usa outra modalidade de rede, conserva essa lacuna de cobertura. Reduz também as permissões da identidade do node: limitar um percurso de rede não justifica manter privilégios sobre dados sem relação com a função. Distingue a identidade cloud da ServiceAccount Kubernetes e atribui cada revisão ao responsável certo. Para etcd, acesso direto de leitura pode revelar dados sensíveis e permitir escalada. Não o classifiques como inofensivo só por excluir escrita, nem pressuponhas que RBAC da API filtrará o conteúdo desse percurso direto. No relatório de hardening, identifica o benchmark e a plataforma a que se aplica. O catálogo CIS distingue famílias para Kubernetes e distribuições geridas; não inventes equivalência entre relatórios. Se um controlo está fora do alcance do scanner, regista quem o opera e que evidência falta. Um controlo manual pendente não se torna aprovado porque os automáticos passaram. Depois de alterar exposição ou configuração, revê os controlos afetados e conserva a ligação entre resultado, versão observada e decisão de aceitação.

Verificar o artefacto que será instalado

Uma assinatura válida só responde à política pretendida se a verificação incluir a identidade autorizada e o issuer esperado. Não adaptes os valores esperados ao certificado inesperado apenas para obter um resultado positivo. Para binários Kubernetes, usa a documentação de publicação correspondente ao artefacto e à versão escolhidos. O procedimento de binários e o de imagens podem indicar identidades diferentes; lê a referência adequada ao tipo de material. Guarda o resultado da verificação com a identidade do conteúdo, sem o transformar numa garantia de ausência de vulnerabilidades. Protege também a passagem entre verificação e instalação. Um script que substitui o ficheiro no mesmo caminho interrompe a relação entre os bytes verificados e os bytes usados. O nome, o tamanho ou o êxito de uma execução não restauram essa relação. Por fim, verifica versão e arquitetura do destino: autenticidade não torna um binário compatível com qualquer node. Num projeto com fornecedor, o handover deve explicar como se escolhe o artefacto, como se verifica a origem e como se demonstra que foi esse conteúdo que entrou no ambiente. O laboratório TLS desta aula não executa Cosign nem valida uma release Kubernetes.

Decidir a passagem para RUN

No caso fictício desta aula, o Secret novo está correto, mas o cliente de fecho ainda recebe o certificado anterior. O operador testou outro host e faltam vinte minutos para a janela terminar. Organiza a investigação por evidência: confirma a rota do cliente, identifica controlador e nome pedido, observa o certificado apresentado e verifica a resposta funcional com os controlos ativos. Mantém uma contingência que também tenha certificado válido e um procedimento de retorno conhecido. Comunica ao comité a diferença entre material preparado e serviço observado. Se a correção não estiver validada a tempo, a decisão pode ser adiar a passagem ou conservar a versão anterior válida com aprovação registada. Retirar validação para obter uma resposta não satisfaz o requisito. O pacote de RUN deve conter configuração efetiva, contactos, matriz de testes, resultados esperados, limites do ensaio e condições de escalada. Resume a aula em quatro perguntas: que percurso foi testado, que identidade foi autenticada, que controlo foi observado e que lacuna permanece? Usa as perguntas de prática para encontrar pressupostos frágeis e volta ao laboratório quando confundires confiança, nome, tempo ou correspondência de chaves.

"""Original local TLS practice. Creates only disposable keys and loopback sockets.
No Kubernetes cluster, controller, CNI, public DNS, production trust store,
revocation service or official exam environment is exercised.
Python 3.13+ and OpenSSL 3.x CLI required. Output never includes private keys.
"""
import json
import platform
import socket
import ssl
import subprocess
import tempfile
import threading
import time
from pathlib import Path


def command(root, *args, check=True):
    p = subprocess.run(['openssl', *args], cwd=root, capture_output=True,
                       text=True, timeout=15)
    if check and p.returncode:
        raise RuntimeError('OpenSSL command failed: ' + args[0] + ': ' + p.stderr)
    return p


def make_material(root):
    for name in ['trusted', 'other']:
        command(root, 'req', '-x509', '-newkey', 'ec', '-pkeyopt',
                'ec_paramgen_curve:prime256v1', '-noenc', '-days', '10',
                '-subj', '/CN=DR temporary ' + name, '-keyout', name + '.key',
                '-out', name + '.pem', '-addext', 'basicConstraints=critical,CA:TRUE',
                '-addext', 'keyUsage=critical,keyCertSign,cRLSign',
                '-addext', 'subjectKeyIdentifier=hash')
    command(root, 'req', '-new', '-newkey', 'ec', '-pkeyopt',
            'ec_paramgen_curve:prime256v1', '-noenc', '-subj', '/CN=DR lab leaf',
            '-keyout', 'leaf.key', '-out', 'leaf.csr')
    (root / 'leaf.ext').write_text('basicConstraints=critical,CA:FALSE\n'
        'keyUsage=critical,digitalSignature\nextendedKeyUsage=serverAuth\n'
        'subjectAltName=DNS:funds.lab.test\nsubjectKeyIdentifier=hash\n'
        'authorityKeyIdentifier=keyid,issuer\n')
    command(root, 'x509', '-req', '-in', 'leaf.csr', '-CA', 'trusted.pem',
            '-CAkey', 'trusted.key', '-set_serial', '101', '-days', '2',
            '-extfile', 'leaf.ext', '-out', 'leaf.pem')
    for p in root.glob('*.key'):
        p.chmod(0o600)


def handshake(root, name, ca):
    server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    server_context.load_cert_chain(root / 'leaf.pem', root / 'leaf.key')
    server_context.minimum_version = ssl.TLSVersion.TLSv1_2
    listener = socket.socket()
    listener.bind(('127.0.0.1', 0))
    listener.listen(1)
    listener.settimeout(5)
    address = listener.getsockname()
    server = {}
    def serve():
        try:
            with listener:
                raw, _ = listener.accept()
                with raw:
                    raw.settimeout(5)
                    with server_context.wrap_socket(raw, server_side=True) as channel:
                        server['request'] = channel.recv(100).decode('ascii')
                        channel.sendall(b'DR synthetic response')
                        server['completed'] = True
        except ssl.SSLError as exc:
            server['tlsError'] = type(exc).__name__
        except Exception as exc:
            server['unexpectedError'] = repr(exc)
    worker = threading.Thread(target=serve, daemon=True)
    worker.start()
    client = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    client.load_verify_locations(cafile=str(root / ca))
    client.minimum_version = ssl.TLSVersion.TLSv1_2
    result = {'serverName': name, 'trustFile': ca,
              'verifyRequired': client.verify_mode == ssl.CERT_REQUIRED,
              'hostnameChecked': client.check_hostname}
    try:
        with socket.create_connection(address, timeout=5) as raw:
            with client.wrap_socket(raw, server_hostname=name) as channel:
                channel.sendall(b'DR request')
                result.update(accepted=True, response=channel.recv(100).decode('ascii'),
                              protocol=channel.version())
    except ssl.SSLCertVerificationError as exc:
        result.update(accepted=False, verificationCode=exc.verify_code,
                      verificationMessage=exc.verify_message)
    finally:
        worker.join(6)
        listener.close()
    if worker.is_alive() or 'unexpectedError' in server:
        raise RuntimeError('Local server did not terminate cleanly: ' + repr(server))
    result['serverCompleted'] = bool(server.get('completed'))
    return result


def run():
    with tempfile.TemporaryDirectory(prefix='dr-cks-tls-') as folder:
        root = Path(folder)
        make_material(root)
        now = int(time.time())
        checks = []
        vectors = [
            ('valid-server', ['-CAfile', 'trusted.pem', '-purpose', 'sslserver',
                              '-verify_hostname', 'funds.lab.test'], True, None),
            ('wrong-name', ['-CAfile', 'trusted.pem', '-verify_hostname', 'other.lab.test'], False, 'hostname mismatch'),
            ('no-ip-san', ['-CAfile', 'trusted.pem', '-verify_ip', '127.0.0.1'], False, 'IP address mismatch'),
            ('untrusted-issuer', ['-CAfile', 'other.pem'], False, 'unable to get local issuer'),
            ('expired-at-reference', ['-CAfile', 'trusted.pem', '-attime', str(now + 4 * 86400)], False, 'has expired'),
            ('before-validity', ['-CAfile', 'trusted.pem', '-attime', str(now - 86400)], False, 'not yet valid'),
            ('wrong-purpose', ['-CAfile', 'trusted.pem', '-purpose', 'sslclient'], False, 'unsuitable certificate purpose'),
        ]
        for name, args, expected, message in vectors:
            p = command(root, 'verify', '-no-CApath', '-no-CAstore', *args, 'leaf.pem', check=False)
            accepted = p.returncode == 0
            if accepted != expected or (message and message not in p.stderr):
                raise AssertionError((name, p.returncode, p.stdout, p.stderr))
            checks.append({'id': name, 'accepted': accepted, 'expectedAccepted': expected,
                           'exitCode': p.returncode, 'reasonChecked': message})
        certificate_public = command(root, 'x509', '-in', 'leaf.pem', '-pubkey', '-noout').stdout
        matching_public = command(root, 'pkey', '-in', 'leaf.key', '-pubout').stdout
        other_public = command(root, 'pkey', '-in', 'other.key', '-pubout').stdout
        assert certificate_public == matching_public and certificate_public != other_public
        checks.append({'id': 'public-key-correspondence', 'matchingPair': True, 'unrelatedPair': False})
        mismatch_rejected = False
        try:
            ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER).load_cert_chain(root / 'leaf.pem', root / 'other.key')
        except ssl.SSLError as exc:
            mismatch_rejected = 'KEY_VALUES_MISMATCH' in str(exc)
        assert mismatch_rejected
        checks.append({'id': 'mismatched-private-key', 'rejected': mismatch_rejected})
        trials = [handshake(root, 'funds.lab.test', 'trusted.pem'),
                  handshake(root, 'other.lab.test', 'trusted.pem'),
                  handshake(root, 'funds.lab.test', 'other.pem')]
        assert [x['accepted'] for x in trials] == [True, False, False]
        assert [x['serverCompleted'] for x in trials] == [True, False, False]
        assert trials[0]['response'] == 'DR synthetic response'
        assert all(x['verifyRequired'] and x['hostnameChecked'] for x in trials)
        assert 'hostname mismatch' in trials[1]['verificationMessage'].lower()
        assert 'issuer' in trials[2]['verificationMessage'].lower()
        report = {'python': platform.python_version(), 'sslLibrary': ssl.OPENSSL_VERSION,
                  'opensslCLI': command(root, 'version').stdout.strip(), 'checks': checks,
                  'handshakes': trials, 'observations': len(checks) + len(trials),
                  'systemClockChanged': False, 'systemTrustStoreChanged': False,
                  'privateKeysPrinted': False, 'clusterExecuted': False,
                  'scope': 'Actual disposable X.509 verification and loopback TLS; no Kubernetes, CNI, Ingress controller, DNS lookup, revocation check or production claim.'}
    report['temporaryMaterialRemoved'] = not Path(folder).exists()
    assert report['temporaryMaterialRemoved']
    return report


if __name__ == '__main__':
    print(json.dumps(run(), indent=2))
NA PRÁTICA

O Secret mudou, mas o cliente de fecho ainda vê o certificado antigo: recolhe endpoint, SNI e material servido antes de aprovar a passagem.

Armadilhas comuns

Confundir objeto aceite com controlo aplicado; testar outro host; retirar validação; aprovar controlos não observados; instalar bytes diferentes dos verificados.

Tópicos relacionados: Identidades e autorização da API · Auditoria e resposta em produção · Artefactos e proveniência

Leva esta ideia contigo

A aceitação liga configuração, percurso, identidade e resultado à mesma revisão observada, conservando os limites da evidência.

Criar conta

Referência: CKS certification and domains · 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.