이번 포스팅에서는 젠킨스를 사용하며 만난 403에러의 원인과 해결과정을 풀어서 설명하겠습니다.
"No valid crumb was included in the request"
(만약 빠른 해결을 원하신다면, jenkins global security settings > csrf disable을 체크하면 됩니다.)
사이트 간 요청 위조(또는 크로스 사이트 요청 위조, 영어: Cross-site request forgery, CSRF, XSRF)는 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말합니다.
출처
개발에 이은 지속적인 통합과 배포를 담당하는 도구입니다.
젠킨스는 Java언어 베이스로 만들어진 CI/CD 도구로, 1400개가 넘는 다양한 플러그인을 지원하기때문에 배포 시 테스트 자동화, 취약점 점검 등 서비스를 배포/운영하는 개발자 및 인프라 관련 업무에서 많은 편리함을 주는 도구입니다.
비슷하게 지속적 통합을 제공하는 도구로는 travisCI,circleCI등이 있습니다.
(jenkins-workflow, 위키백과)
소스코드의 업데이트를 확인하기 위해 Github의 Webhook이나 다른 연동 도구를 통해서 젠킨스와 연결해야합니다.
이 과정에서 아래와 같은 에러문구를 확인했고, 어떤 이유에서 github가 jenkins의 요청을 거부했다는 것을 알 수 있었습니다.
에러메시지 : No valid crumb was included in the request — 요청에 유효한 Crumb가 존재하지 않는다.
에러메시지에는 crumb라는 특이한 단어가 등장합니다. 직역하면 빵부스러기, 구글에 검색해도 특별한 내용이 나오진 않습니다만, Jenkins crumb라고 검색하니 jenkins의 보안관련 문서들이 보였습니다.
crumb란, Jenkins에서 github등 소스코드 리포지토리의 데이터를 가져올 때 요청의 주체가 자신임을 확인하도록 하는 IP+salt로 이루어진 쿠키입니다.
즉, crumb를 요청헤더에 쿠키로 추가하므로써 CSRF 공격을 방지할 수 있는것이죠!
반대로 Jenkins의 CSRF 보안 설정을 켜놓으면 어떤 문제로 인해 crumb의 유효성이 사라졌을 때 403에러와 함께 일을 진행하지 못하는 사태가 발생합니다. 참고로 젠킨스는 2.0부터 CSRF protection이 기본 보안설정입니다.
(하지만 위에서도 말씀드렸듯, CSRF 보안 설정을 체크헤제하면 에러는 말끔히 사라집니다.)
위에서 이야기한 어떤 문제를 찾는건 시간이 꽤 걸렸습니다...
crumb에 대해 잘 모를 때 github의 설정문제라고 생각하고 자꾸 삽질을 했는데요,
has no valid crumb 에러가 발생하는 이유는 대부분 프록시 설정 문제입니다.
crumb는 IP+salt의 조합이라고 설명했는데요, "has no valid crumb"라는 에러메시지를 통해 제가 Jenkins를 서비스하는 과정에서 요청 IP가 변경되는 과정이 있는지 생각해봐야합니다.
대부분 젠킨스 서버와 프록시가 있거나 캐싱 서버가 있어 IP주소가 변경되는 경우가 많습니다. 이때문에 Crumb가 유효성을 상실하게 됩니다.
문제를 찾고 이부분을 어떻게 해결할까 하다가 Jenkins Crumb정책 관련한 소스코드를 보면서 답을 찾았습니다.
Jenkins의 소스코드입니다. Jenkins DefaultCrumbIssuer.java
public class DefaultCrumbIssuer extends CrumbIssuer {
...
@DataBoundConstructor
public DefaultCrumbIssuer(boolean excludeClientIPFromCrumb) {
this.excludeClientIPFromCrumb = excludeClientIPFromCrumb;
initializeMessageDigest();
}
...
@Override
protected synchronized String issueCrumb(ServletRequest request, String salt) {
...
if (!isExcludeClientIPFromCrumb()) {
buffer.append(getClientIP(req));
}
...
}
...
private String getClientIP(HttpServletRequest req) {
String defaultAddress = req.getRemoteAddr();
String forwarded = req.getHeader(X_FORWARDED_FOR);
if (forwarded != null) {
String[] hopList = forwarded.split(",");
if (hopList.length >= 1) {
return hopList[0];
}
}
return defaultAddress;
}
...
XFF란? X-forwarded-for의 약자로, 지금 상황처럼 프록시나 다른 중간 서버로 인해 IP가 변경될 때, Origin IP를 식별하는 표준 헤더입니다. 위키백과
getClientIP메서드에서 XFF헤더가 있는 경우 hopList를 통해 중간에 거쳐갈 IP주소 리스트를 리턴하도록 작성되었습니다.
즉, 젠킨스 설정을 통해 문제를 해결할 수 있는것이죠!
excludeClientIPFromCrumb변수는 위의 issueCrumb메서드에서 false일 때 getClientIp메서드를 통해 XFF헤더를 적용합니다.
즉, DefaultCrumbIssuer의 생성자로 false를 지정하게 되면 XFF가 지정되고, 프록시로 인한 IP변조 문제가 해결되는것이죠.
이제부터는 해결과정입니다.
젠킨스 그루비 설정파일을 지정하기 위해 젠킨스 홈에서 설정파일 만들어줍니다.
(default: /var/lib/jenkins/init.goovy.d/myconf.groovy)
import hudson.security.csrf.DefaultCrumbIssuer
import jenkins.model.Jenkins
// 중단모드일 경우 시행하면 안됨
if(!Jenkins.instance.isQuietingDown()){
//CSRF 설정이 있는 경우
if(Jenkins.instance.getCrumbIssuer()!=null){
def instance = Jenkins.instance
// DefaultCrumbIssuer(false) : XFF헤더 정의
instance.setCrumbIssuer(new DefaultCrumbIssuer(false))
instance.save()
println 'excludeClientIPfromCrumb set: false'
}else{
println 'Nothing changed'
}
}
이후 젠킨스를 재시작합니다.
$ service jenkins restart
CSRF와 프록시 설정으로 인해 발생하는 403에러에 대한 해결과정이었습니다!