Jenkins Publish over SSH 인증시 BapPublisherException 오류 원인과 해결책

Jenkins Publish over SSH 플러그인을 제대로 설치도 하고 서버에 공개키 세팅해 주고, Jenkins에 개인키 설정까지 다 마쳤는데, 인증이 언젠가부터 안되는 분들 계시죠? Jenkins에서 BapPublisherException 발견하셨나요? 저도 원인 파악하느라 고생을 했는데, 공유합니다.

Test Configuration 실행하면 BapPublisherException 예외발생

혹시 아래와 같은 명령어로 인증서 만드셨나요? 그런데 예외가 발생하지는 않으셨나요?

만약에 오류를 만나지 않으셨다면, 아마 사용하는 서버의 OpenSSH 버전이 8.8보다 낮을 가능성이 있습니다.

ssh-keygen -t rsa -b 4096 -m PEM
ShellScript

분명 SHA256까지 적용이 됐는데 말이죠. 심지어 키 크기까지 4096bit로 잡아줬는데, Test Configuration 눌렀는데 아래와 같은 오류 메시지가 나왔나요?

jenkins.plugins.publish_over.BapPublisherException: Failed to connect and initialize SSH connection. Message: [Failed to connect session for config [My Server]. Message [Auth fail]]

일단 생각하시는대로 키는 제대로 생성된 게 맞습니다. 그런데 왜 안돼냐 말이죠! 그건 Jsch 때문인데, 자세한 내용 살펴보겠습니다.

OpenSSH 8.8 SHA-1 미지원 결정

우선 OpenSSH 8.8부터 SHA-1 해시 알고리즘을 사용하는 RSA 시그니처를 지원하지 않기로 결정했습니다. SHA-1은 해시 충돌이 발생할 수 있습니다. 암호학적으로 부서졌다는 거죠. 한마디로 요약하면 보안상 안전하지 않으니, 앞으로는 허용하지 않겠다는 거겠죠.

그런데 Publish over SSH는 JCraft에서 만든 Jsch라는 라이브러리를 사용하여 개발했습니다. 문제는 Jsch 라이브러리가 최신 기술들을 반영하고 있지 않다는 겁니다. rsa-sha2-256이나 rsa-sha2-512를 사용할 수도 없고, ed25519도 지원하지 않습니다. 그래서 Publish over SSH를 사용해서 연결을 테스트 하면 서버에 아래와 같은 로그가 찍히며 인증이 되지 않습니다. 맞습니다. OpenSSH에서 거부하는 장면입니다. 인증키를 SHA-2인 SHA256으로 생성해도 SHA-1으로 처리되는 순간입니다.

Dec 19 16:32:51 instance-20210904-0100 sshd[148076]: userauth_pubkey: key type ssh-rsa not in PubkeyAcceptedAlgorithms [preauth]

해결책: ECDSA 사용하기

OpenSSH 8.8 배포 설명에서는 ecdsa나 ed25519를 대신 사용하라고 권장하고 있습니다. 다행이 Jsch에서 ecdsa를 지원합니다. 아래와 같이 개인키/공개키 쌍을 생성해서 사용하면 제대로 인증이 되는 것을 확인할 수 있습니다.

ssh-keygen -t ecdsa -b 521 -m PEM
ShellScript

일단 당분간 큰 문제가 없는 한 ecdsa를 이용하면 되지 않을까 싶습니다. Publish over SSH 깃헙에도 해당 이슈가 아직 열려있습니다. 관심 있는 분들의 기여가 있다면 또 개선이 되지 않을까 싶습니다.

문제 해결에 도움이 된 링크 공유

OpenSSH 8.8 릴리즈 문서Github Publish over SSH의 인증 오류 관련 이슈를 참고했습니다.

같이 읽으면 좋은 글

Jenkins alpine 컨테이너에서 NodeJS 프로젝트 빌드하는 3가지 방법

Jenkins alpine 컨테이너에서 NodeJS 프로젝트를 빌드하려는데 오류가 생기나요? Jenkins alpine 이미지를 활용해서 Jenkins를 구동하지 말아야 하는 이유를 설명해 드립니다.

Jenkins alpine 이미지 사용 비추천

결론부터 일단 말씀드리면, Jenkins를 Docker로 띄워서 사용할 것이고, NodeJS 프로젝트를 빌드할 계획이라면 Alpine Jenkins 도커 이미지를 사용하지 마시길 권합니다.

이유: musl-libc 이슈

alpine은 경량 musl-libc 라이브러리를 사용하는데, Jenkins의 NodeJS 플러그인은 glibc로 컴파일 되어 있다고 합니다. 굳이 alpine 이미지를 사용해야 한다면 플러그인 소스 코드를 구해서 musl-libc로 컴파일하고 설치하면 될 수도 있겠지만, 매우 귀찮은 작업이 될 수 있습니다.

저의 경우는 NVM Wrapper 플러그인과 NodeJS 플러그인을 모두 Jenkins에 설치해서 시도해 봤는데, 플러그인 설치와 설정까지는 정상적으로 되는데, 결정적으로 실행이 안 됩니다. node 명령어를 실행하면, Not Found를 외치며 실행되지 않습니다.

심지어 node 파일이 있는 디렉토리까지 찾아가서 ./node를 입력해 보았지만 찾을 수 없다고 합니다. 정말 황당했지만, 어쩌겠습니까.

해결책

Debian 기반의 Jenkins 이미지 활용: 추천

아래와 같이 Debian 기반의 도커 이미지로 Jenkins를 사용하면 아무 문제 없이 해당 플러그인을 사용할 수 있습니다.

docker pull jenkins/jenkins:lts-jdk11
ShellScript

Alpine에 Glibc를 설치

Alpine 리눅스에 gblic를 설치하는 것이 하나의 방법이긴 하지만, 이미지 재구성이 필요하고 Alpine을 사용하는 이유가 사라지게 됩니다. 그럴 바에야 차라리 Debian 이미지를 사용하는 편이 더 나아 보입니다.

Alpine에서 플러그인 소스 컴파일

플러그인 소스 코드를 다운 받아서 musl-libc 기반으로 컴파일을 새로 한 후 수동 설치하는 방법입니다. 이와 같은 문제가 생기는 플러그인들을 모두 일일히 컴파일 하는 건 꽤 고단한 작업이 될 수 있습니다. 서버 자원이 너무 부족해서 꼭 alpine linux를 사용해야 하는 경우가 아니라면, Jenkins 구동은 Debian 이미지 사용하시길 권합니다.

관련 링크

참고했던 자료를 아래에 링크 걸어놓았습니다. Stackoverflow의 “Nodejs with jenkins plugin not executable in alpine image and displays “symbol not found” 답글 달아주신 valiano님 고맙습니다.

같이 읽으면 좋은 글

Bitbucket 저장소와 Jenkins webhook 연동 – proxy

Jenkins와 Bitbucket Repository를 Webhook으로 연동했을 때의 문제는 webhook 조건을 pull request merged로 설정하면 브랜치 여부에 관계없이 webhook을 날린다는 것이다. 필자가 앞서 포스팅 한 글(https://osg.kr/archives/80)에서 사용하는 방법도 마찬가지다.

안드로이드 프로젝트의 dev 브랜치가 pull request 됐을 때 개발용 앱을 빌드하고, master 브랜치에는 tag가 push 됐을 때 상용 앱을 빌드하도록 하고 싶다면? 어떻게 해야 할까? 이는 실제 필자가 서비스 개발에서 활용하고 있는 방법이다.

필자는 Proxy 서버를 Node.js로 간단히 구축하여 개발서버에서 구동하고 있다. Bitbucket에서는 webhook을 날리는 request body를 로그로 남겨준다. 로그에 기록된 body를 참조해서 해당 json을 파싱해서 활용하면 된다.

프록시 작동 원리는 다음과 같다. 개발용 앱 빌드의 경우 Bitbucket에서는 브랜치에 상관없이 pull request가 merge되면 webhook을 Node.js로 구축한 Proxy 서버에 보낸다. 프록시 서버는 body의 내용을 확인해서 dev 브랜치로 머지된 것인 경우에만 Jenkins 서버로 다시 Webhook을 걸어준다.

상용 앱 릴리즈 시에는 Bitbucket에 tag가 push되면 Proxy로 일단 webhook이 걸린다. 프록시 서버에서 body의 내용을 확인해서 master branch에 태깅됐을 경우에만 Jenkins로 webhook을 걸어준다.

단순히 도식화하면 아래와 같다.

Bitbucket Webhook > Node.js proxy > Jenkins task item

물론 Proxy 서버는 꼭 Node.js를 쓸 필요는 없다. 자신이 쓰기 편한 프레임워크를 사용하면 된다.