jenkins의 원격 배포에 대해 검색하면 대부분의 포스팅들이 SSH를 통해 build artifacts를 전송하는 Publish Over SSH Plugin을 사용한다. 하지만 2022년 1월 12일, Jenkins에서 제공하는 제품(플러그인)의 취약점에 대한 보안 권고가 올라왔다. 그리고 Publish Over SSH plugin은 권고문을 발행한 시점에서 해결책이 없어서 더 이상 플러그인을 사용할 수 없게 되었다.
이 글에서는, 보안 권고 사항에 올라온 플러그인 중 Publish Over SSH Plugin의 취약점과, 이 Plugin을 사용하지 않고 scp를 이용한 배포 방식에 대해 포스팅한다.(해석에 문제가 있거나, 수정해야할 부분 말씀해주시면 감사하겠습니다!🙏🏻)
Jenkins EC2 Server에서 gradle build된 jar 파일을 다른 EC2 Server에 배포 후 실행하는 과정입니다.
jenkins의 권고에 따른 Publish Over SSH Plugin의 취약점
1. Stored XSS vulnerability [참고]
Publish Over SSH Plugin 1.22 버전 이하에서 SSH Server Name을 등록하는 과정은 필수다. 그 결과 전체/관리자 권한을 가진 공격자가 이용할 수 있는 stored XSS vulnerability이 발생한다. 이 플러그인이 SSH Server Name을 입력하는 값에 대한 정확한 검증이 없기 때문에 시스템 전체 권한을 가질 스크립트를 가질 수 있다는 것으로 이해했다. 이 Name은 Jenkins 내에서 환경 변수명으로 이용된다.
2. CSRF 취약성 [참고] & 권한 검사 누락
Publish Over SSH Plugin 1.22 버전 이하에서 SSH Server를 등록할 때, Test Connection을 통해서 미리 정상적으로 연결이 되는지 확인하는 버튼이 있다. 이 테스트 연결 과정에서 사용 권한 검사를 수행하지 않는다. 따라서 전체/읽기 권한이 있는 공격자는 특정 credentials을 이용해 특정 SSH로 연결할 수 있으며, 연결 테스트는 POST 요청이 필요없어 CSRF 취약성이 발생한다.
3. path traversal vulnerability [참고]
Publish Over SSH Plugin 1.22 버전 이하에서 원격 서버로 보낼 파일의 경로를 입력하는 곳이 있다. 이 파일이 존재하든, 하지 않든 파일 이름의 유효성을 검사하게 되는데, item/configurate 권한을 가진 공격자가 jenkins controller file들을 발견할 수 있는 path traversal 취약성을 가진다.
4. 일반 텍스트로 저장된 암호키
Publish Over SSH Plugin 1.22 버전 이하에서 jenkins/plugins/publish_over_ssh/BapSshPublisherPlugin.xml라는 global configuration 파일에 암호키를 암호화하지 않고 저장한다. 이는 Jenkins controller file system 구성의 일부다. 따라서 Jenkins controller file system에 접근할 수 있는 사용자가 볼 수 있다.
이 취약점들이 권고가 올라온 시점에 해결되지 않았기에, 이 Plugin의 사용이 중단된 것이다.
이 플러그인이 어떻게 사용됐는지는 여기를 참고하자.
원격 배포의 대체방법으로 어떤 것을 사용할 것인가?
대체할 수 있는 플러그인이 어떤 것들이 있는지 알아보았다. build artifacts를 FTP나 윈도우에 공유할 수 있는 플러그인도 있었지만, 원했던 방식이 아니라서 넘어갔고 SCP publisher라는 SSH(SCP) 프로토콜을 사용하여 build artifacts를 저장소 사이트에 업로드하는 플러그인은 2017년 보안 권고를 받은 이후 업데이트되지 않아서 넘어갔다. [어떻게?]
이 보안 이슈 이후에, 다른 사람들은 어떤 플러그인을 대체했는지 알아봤을 땐, SSH Pipeline Steps을 이용하자는 커뮤니티의 내용을 봤었고, 이 외에, ssh-agent plugin도 존재했는데 credential을 입력하고 스크립트를 jenkins file로 지정하는 방식이었다. (저는 jenkins를 배우고 실습한지 이제 2주가 되어가는 시점이라 파이프라인, 젠킨스 파일 활용을 할 줄 아시는 분들은 이 플러그인을 사용해도 될 것 같ㅇ습니다!)
그래서 나는 SSH 프로토콜을 사용하여 원격으로 셸 명령을 실행하는 SSH Plugin을 사용하기로 했다.
원격 서버로 scp를 통해 파일을 배포완료 후 셸 스크립트를 실행시켜 배포하는 방식을 해보기로 했다.
SSH Plugin 사용하기
1. Jenkins 관리 - Plugin Manager - 설치 가능에서 SSH plugin 을 설치한다.
2. Jenkins 관리 - Manage Credential 에서 Add Credential 클릭

3. global credentials의 내용 입력후 ok를 눌러 저장

4. Jenkins 관리 - 시스템 설정 - SSH remote hosts 단락으로 이동 후 추가버튼 클릭하여 아래의 화면의 내용 채우기

5. 해당하는 프로젝트 - 구성 - Build 단락에서 내용 추가

Build의 내용을 추가할 경우 그 순서대로 작동하게 된다. 우선 add Build Step을 클릭해 Invoke Gradle Script를 선택한다.
wrapper의 location이 root에 존재할 경우 ${workspace}를 입력하면 되고, 그렇지 않은 경우 gradle 폴더가 있는 경로를 입력하면 된다. 그 후 task에 gradlew로 어떤 작업을 할 것인지 입력한다.
입력이 끝나고 나면, 빌드가 완료된 후 젠킨스 서버에서 원격 서버로 build artifacts를 전송해줘야한다. 기존에는 Publish Over SSH가 대신해줬지만, 더는 없기에 이 단계부터 직접 입력을 해줘야한다. 이 때, add Build step을 눌러 Execute Shell을 추가한다.

나는 scp를 이용해 파일을 원격 서버로 전송했다. 이 때 사용한 명령어는 다음과 같다.
scp -v -o StrictHostKeyChecking=no -i [젠킨스 서버 내 원격 서버 pem파일 경로] [젠킨스 서버 내 원격서버로 전송할 파일경로] [username]@[원격서버주소]:[파일을 저장할 경로]
-i 옵션은 공개 키 인증을 위한 개인 키 값 파일을 두기 위한 것이다. pem 경로가 적힌 것이 그 이유다.
-v 옵션은 verbose 모드로 scp 명령어가 실행될 때 실행 상세 내용을 출력하기 위한 것이라, 없어도 상관은 없다.
-o 옵션은 뒤에오는 ssh_option을 전달하는데 사용하기 위해서다.
그렇다면 ssh_option에 전달되는 StrictHostKeyChecking = no는 무엇일까?
이 옵션을 추가하지 않고 실행을 하면 Host key verification failed가 발생한다. 특정 호스트에 처음으로 접근할 때, 연결을 계속할 것인지 yes/no를 입력하라고 나온다. yes를 입력하면 ~/.ssh/known_hosts에 호스트 정보가 추가되면서 이후에 발생하지 않는데, 서버에 직접 접속해서 이 명령어를 실행해주고 ssh/known_hosts에 추가되도록 유발할 수 있지만 앞으로도 계속 이런 방식을 사용해야 하거나, 여러대의 서버로 배포하는 상황이라면 적절하지 않을 것이다.
따라서 StrictHostKeyChecking=no 옵션을 사용한다는 것은, yes/no를 입력하지 않고 바로 추가하면서 서버에 접속하게 된다.
이 외에 이 이슈를 해결할 다양한 방법은 여기를 참고하자.
만약 저 명령어를 그대로 전달한다면, Permission denied가 발생할 수 있다. 이 해결법은 여러가지로, sudo를 통해 실행하거나 pem 파일의 권한을 지정해주면된다(chmod).
sudo를 통해 실행할 경우 no tty present and no askpass program specified 에러가 발생한다.

sudo 명령이 암호가 필요한 명령을 실행하려고 하지만 sudo가 사용자에게 암호를 입력하라는 메시지를 표시할 수 있는 tty에 액세스할 수 없는 경우다. 젠킨스가 스크립트를 실행하면서 암호가 필요한데, tty를 찾을 수 없기 때문에 sudo는 다시 askpass 메서드로 폴백하지만 구성된 askpass 명령을 찾을 수 없는 것. 그렇다면 암호가 필요없다고 명시적으로 선언을 해줘야한다.
이는 /etc/sudoers 를 편집해주면 된다. 나는 vi로 편집을 했고, 제일 하단에 다음을 추가했다. 젠킨스 유저에게 암호 요구를 하지 않는 것.
jenkins ALL=(ALL) NOPASSWD: ALL
그리고 저장을 하고 나오면된다. 자 이렇게 되면 실행할 셸 스크립트의 이슈는 해결되었다.
이제 원격 서버로 파일을 전송 후 배포를 하도록 해보자.

add Build Step을 클릭 후, Execute shell script on remote host using ssh를 클릭하면 된다. SSH site에 4번에서 추가해둔 SSH remote host를 선택하고 실행할 셸 스크립트를 작성하면 된다. 나는 gradle의 Spirng Boot 방식을 이용했기에, 해당하는 jar이 실행중일 경우 kill 하고, nohup로 다시 실행하는 .sh 파일을 실행하도록 만들었다.
이렇게 Publish Over SSH Plugin 대신 SSH Plugin을 이용해 배포 후 실행하도록 하는 것이 성공했다.
이 방식은, 플러그인을 대신해 직접 제어한 경우라, 보안상의 문제가 있을 수 있으니 지적해주시면 너무 감사하겠습니다 🙏🏻
발생했던 추가 이슈
1. 원격 shell 실행 시 job이 끝나지 않는다
나는 [nohup java -jar 파일이름.jar] 명령어를 통해 원격 shell에서 백그라운드에서 배포 후 실행을 시키고 job을 끝내려했다.

하지만, 젠킨스에서 build 실행하니 애플리케이션을 실행시키고 job이 끝나지 않았다.
SSH로 원격 스크립트를 실행하면 stdoutput이 닫히거나 timeout이 발생할 때 까지 스크립트는 열려있게 된다. 그래서script로 백그라운드 작업 수행시에는, 아래와 같이 모든 output을 redirect 해줘야 스크립트가 바로 종료가 된다.
nohup java -jar [파일이름.jar] > /dev/null 2>&1 &
> /dev/null: stdout을 출력을 기록하지 않게 /dev/null로 이동
2>&1 : stderr도 stdout으로 이동
&: 백그라운드로 실행
2. Execute Shell에서 Exception:Auth Fail

이는 내가 3번 global Credential을 생성할 때 username을 jenkins를 입력해뒀다. 원격 서버는 jenkins 사용자가 없는데 말이다.
참고자료:
https://www.shell-tips.com/linux/sudo-no-tty-present-and-no-askpass-program-specified/
https://newly0513.tistory.com/145
https://stackoverflow.com/questions/50105414/scp-command-fails-to-copy-host-key-verification-failed
https://goateedev.tistory.com/48
https://wikidocs.net/20643