Jenkins no Kubernetes: unable to find valid certification path to requested target
Faaaaala meus queridos! Eis que eu ressurjo das cinzas para falar um pouco do meu novo projeto. Atualmente, estou montando um homelab com 2x Optiplex rodando Proxmox e um cluster Kubernetes (tenho documentado tudo no meu Instagram).
Neste processo, criei uma Certificate Authority privada utilizando o Vault dentro do cluster, usado para assinar todos os certificados das outras aplicações (Gitea, Jenkins, Grafana, etc). Ao tentar configurar o plugin do Gitea para receber webhooks, me deparei com o problema clássico: unable to find valid certification path to requested target.
O que significa o erro “unable to find valid certification path to requested target”?
Isso significa que o Jenkins está tentando se conectar com uma determinada URL, mas o TLS handshake está falhando, já que ele não confia na CA que assinou o certificado.
É comum esse cenário quando usamos uma CA privada, então o que precisamos fazer é importar esse certificado no Jenkins.
Como resolver este erro?
Primeiro é importante salientar que eu fiz o deploy do Jenkins no Kubernetes utilizando este Helm Chart. Portanto, meu foco era tentar encontrar uma solução apenas modificando os values.yaml do chart.
Depois de muitos testes, cheguei em mais ou menos o seguinte:
- Criar uma keystore do Java personalizando contendo o certificado da CA privada.
- Criar um secret no Kubernetes contendo esse arquivo.
- Adicionar esse secret no chart to Jenkins.
- Alterar os parâmetros de inicialização do Java dentro do chart para usar essa keystore.
Vamos lá então!
Criando uma keystore customizada
A primeira coisa é copiar a keystore atualmente usada pelo pod do Jenkins para a minha máquina pessoal, pra que então eu pudesse personalizar ela, e então modificar o resto.
$ k exec -it -n jenkins jenkins-0 sh
$ cd $JAVA_HOME/lib/security
$ ls -l
total 400
-rw-r--r-- 1 root root 2488 Jul 13 17:50 blocked.certs
-rw-r--r-- 1 root root 155878 Jul 13 17:50 cacerts
-rw-r--r-- 1 root root 9596 Jul 13 17:50 default.policy
-rw-r--r-- 1 root root 232578 Jul 13 17:50 public_suffix_list.dat
Você vai ver um arquivo chamado “cacerts” que é a keystore padrão usada pelo Java. Agora vamos sair do pod e copiar esse arquivo para nossa máquina pessoal no diretório tmp.
$ cd /tmp
$ k cp -n jenkins jenkins-0:/opt/java/openjdk/lib/security/cacerts keystore.jks
$ ls -lh keystore.jks
-rw-rw-r-- 1 mateus mateus 153K Aug 7 14:44 keystore.jks
Você pode listar todos os certificados contidos nesse arquivo com o seguinte comando:
$ keytool -list -keystore keystore.jks
A senha padrão dessa keystore é changeit.
Acredito que você tenha o certificado da CA em um .pem ou .crt, então basta rodar o seguinte comando:
$ keytool -import \
-trustcacerts \
-alias <apelido> \
-file <caminho>/certificado.crt \
-keystore keystore.jks
O próximo passo é criar um secret contendo esse arquivo.
Criando secret e modificando o chart
Eu criei um secret contendo a senha e a keystore, mas ainda não consegui ver 100% como reutilizar a senha no chart.
$ k create secret generic -n jenkins jenkins-https-jks \
--from-literal https-jks-password=changeit \
--from-file jenkins-jks-file=keystore.jks
Se você der um describe no secret, vai ver duas keys, uma com a senha e outra com a keystore.
$ k describe secret -n jenkins jenkins-https-jks
Name: jenkins-https-jks
Namespace: jenkins
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
https-jks-password: 8 bytes
jenkins-jks-file: 156877 bytes
O próximo passo era montar o secret dentro do Jenkins. Olhando o values.yaml do chart, eu achei uma flag chamada additionalExistingSecrets que é basicamente para informar secrets já criados e que você quer montar dentro do pod.
Coloquei dessa forma então:
...
additionalExistingSecrets:
- name: jenkins-https-jks
keyName: jenkins-jks-file
- name: jenkins-https-jks
keyName: https-jks-password
...
Depois de modificar o values, apenas fiz um upgrade do chart com tudo atualizado.
$ helm upgrade --install \
--namespace jenkins \
--create-namespace jenkins jenkins/jenkins \
-f values.yaml
Os secrets são montados como arquivos dentro do /run/secrets/additional.
$ k exec -it -n jenkins jenkins-0 sh
$ ls -lh /run/secrets/additional
total 0
lrwxrwxrwx 1 root jenkins 27 Aug 6 18:29 chart-admin-password -> ..data/chart-admin-password
lrwxrwxrwx 1 root jenkins 27 Aug 6 18:29 chart-admin-username -> ..data/chart-admin-username
lrwxrwxrwx 1 root jenkins 43 Aug 6 18:29 jenkins-https-jks-https-jks-password -> ..data/jenkins-https-jks-https-jks-password
lrwxrwxrwx 1 root jenkins 41 Aug 6 18:29 jenkins-https-jks-jenkins-jks-file -> ..data/jenkins-https-jks-jenkins-jks-file
Você pode ainda confirmar se seu certificado está lá com o seguinte comando:
$ keytool -list -keystore jenkins-https-jks-jenkins-jks-file | grep -i mateus
Enter keystore password: *********
mateusmullerme, Aug 4, 2022, trustedCertEntry,
Chegamos na reta final! Agora é só modificar as opções do Java para usar essa nova keystore.
Alterando a keystore padrão do Java
No mesmo values.yaml do chart, eu achei uma opção chamada javaOpts ao qual podemos passar parâmetros que serão inseridos na variável ambiente JAVA_OPTS.
Modifiquei para ficar dessa forma:
...
javaOpts: >
-Djavax.net.ssl.trustStore=/run/secrets/additional/jenkins-https-jks-jenkins-jks-file
-Djavax.net.ssl.trustStorePassword=changeit
...
E aí mandei um novo upgrade:
$ helm upgrade --install \
--namespace jenkins \
--create-namespace jenkins jenkins/jenkins \
-f values.yaml
E boom! Prontinho. Não tive mais problemas.
Se você der um describe no pod, dá pra ter uma ideia do que foi modificado.
$ k describe po -n jenkins jenkins-0
...
Environment:
SECRETS: /run/secrets/additional
POD_NAME: jenkins-0 (v1:metadata.name)
JAVA_OPTS: -Dcasc.reload.token=$(POD_NAME) -Djavax.net.ssl.trustStore=/run/secrets/additional/jenkins-https-jks-jenkins-jks-file -Djavax.net.ssl.trustStorePassword=changeit
JENKINS_OPTS: --webroot=/var/jenkins_cache/war
JENKINS_SLAVE_AGENT_PORT: 50000
CASC_JENKINS_CONFIG: /var/jenkins_home/casc_configs
...
É claro que não é o ideal informar esse secret direto no values.yaml. Talvez seria melhor criar uma variável ambiente com o valor do secret, e passar a variável ambiente nos parâmetros do Java, mas ainda não consegui parar pra refazer isso.
Enfim, espero que ajude! Perdi um tempinho nisso e decidi compartilhar a luta aqui rsrs. Grande abs!
Buy me a coffee