정말 찾아내기가 너무 어려웠던... 배포에서의 시크릿키 문제..
정말 거의 일주일 내내 힘들게 원인을 찾았고, 겨우 찾아냈다!
나중에 절대 겪지 않기 위해 작성해보는 이야기!
나는 React + Spring Boot 로 풀스택 개발을 진행했고, 이 분 블로그를 보고 열심히 잘 따라했다.
[Full Stack 배포] GitHub Action을 사용하여 AWS EC2에 React, Springboot 자동 배포하기
팀 프로젝트를 위한 react + springboot 배포 강좌
velog.io
배포하면서 여러 문제점이 있었지만, 가장 찾기가 힘들었던 큰 문제점을 적어보겠다!
[배포 방식]
로컬에선 아주 잘 진행되던 프로젝트 > 다 만들어서 이제 AWS EC2에 배포할 시간!
그렇지만 앞으로 계속해서 기능을 하나씩 추가하면서 정말 완벽한 서비스를 구축하고 싶었고, 실무에서 가장 필요할 CI/CD를 직접 구현해보고 싶었다! 젠킨스도 고민했지만, 우선은 Github Action 으로 CI/CD를 구축했다.
커밋하면 프로젝트 빌드 후, Docker Image를 생성하고 Docker Hub 에 올리면 EC2에서 허브에 있는 이미지를 가져오고, docker compose 로 서비스를 재시작하는 방식!
이 프로젝트는 내 대표 개인 프로젝트로 깃허브에 공개해야 했기 때문에, 중요한 정보들은 무조건 .env 파일에 넣어줬다.
(실제로 로컬에서도 .env 파일에 넣고 잘 진행해왔었다.)
이제 이 값들을
1) Github Actions 의 시크릿에도 잘 넣어뒀고,
2) AWS EC2에 있는 docker-compose.yml 에도 이렇게 .env 파일을 사용하겠다고 잘 작성해줬고,
version: '3'
services:
backend:
image: xx/stock-be
ports:
- "8080:8080"
env_file:
- .env
networks:
- network
frontend:
image: xx/stock-fe
ports:
- "80:80"
depends_on:
- backend
networks:
- network
networks:
network:
3) .github/workflows/deploy.yml 파일에도 아래처럼 깃허브 시크릿에 작성한 값을 토대로 .env 파일을 생성하는 것도 적어줬다.
=>> 사실 먼저 말하자면 여기가 문제였음. <<'EOF' > 로 작성해야 함.
# 1. .env 파일 생성
cat <<EOF > /home/ubuntu/.env
NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}
NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}
MYSQL_URL=${{ secrets.MYSQL_URL }}
MYSQL_USERNAME=${{ secrets.MYSQL_USERNAME }}
MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}
CORS_ALLOWED_ORIGINS=${{ secrets.CORS_ALLOWED_ORIGINS }}
EOF
스프링부트 프로젝트 내에서 조금 변경된게 있다면 이부분!
dotenv.get 대신 System.getenv 로 바꿔줬다.
@SpringBootApplication
@EnableScheduling
public class StockManagementApplication {
public static void main(String[] args) {
/* .env 파일을 로드하여 환경변수로 설정할 때의 내용
Dotenv dotenv = Dotenv.configure().load();
// 이후 로드된 환경변수를 사용하여 애플리케이션 설정
System.setProperty("NAVER_CLIENT_ID", dotenv.get("NAVER_CLIENT_ID"));
System.setProperty("NAVER_CLIENT_SECRET", dotenv.get("NAVER_CLIENT_SECRET"));
System.setProperty("NAVER_API_CALLER_IP", dotenv.get("NAVER_API_CALLER_IP"));
System.setProperty("MYSQL_URL", dotenv.get("MYSQL_URL"));
System.setProperty("MYSQL_PASSWORD", dotenv.get("MYSQL_PASSWORD"));
System.setProperty("MYSQL_USERNAME", dotenv.get("MYSQL_USERNAME"));
SpringApplication.run(StockManagementApplication.class, args);
*/
// 환경 변수를 읽어 애플리케이션에 설정할 때의 내용 (EC2의 .env 에서 설정된 환경변수)
System.setProperty("NAVER_CLIENT_ID", System.getenv("NAVER_CLIENT_ID"));
System.setProperty("NAVER_CLIENT_SECRET", System.getenv("NAVER_CLIENT_SECRET"));
System.setProperty("MYSQL_URL", System.getenv("MYSQL_URL"));
System.setProperty("MYSQL_USERNAME", System.getenv("MYSQL_USERNAME"));
System.setProperty("MYSQL_PASSWORD", System.getenv("MYSQL_PASSWORD"));
System.setProperty("CORS_ALLOWED_ORIGINS", System.getenv("CORS_ALLOWED_ORIGINS"));
SpringApplication.run(StockManagementApplication.class, args);
}
}
[문제 상황]
1. 네이버 커머스 API 의 인증토큰을 발급받기 위한 '클라이언트 시크릿' 코드가 자꾸 abash4 라는 값으로 바뀜.
2. 그래서 네이버 커머스 API를 불러올 수가 없음.
3. CI/CD 적용하기 전인, 직접 빌드해서 서버에 올리는 식으로 직접 배포하거나, 로컬에서 진행할 때에는 한 번도 문제가 생긴 적이 없음.
4. 결국 지금 CI/CD 적용하기 시작하니까 문제가 된 것..!
[해결]
문제의 원인은 클라이언트 시크릿 값이 $ 로 시작하기 때문이었다!
아래가 바로 시크릿 값 형식이다.
$2a$04$XXXXXXxxxxxxXXXXX4Yfne
이 값은 셸에서 실행될 때 $2와 $04 등으로 인식되어, 실제로 의도한 값이 아니라 빈 값이나 다른 값(예를 들어 스크립트 이름 등)으로 대체될 가능성이 크다고 한다.
구체적으로,
- $2a는 $2 (두 번째 위치 매개변수)가 없으면 빈 값으로 치환되고 그 뒤에 a가 붙어 "a"가 된다.
- $04는 아마도 ${0}4로 해석되어, $0 (실행 중인 셸 또는 스크립트의 이름, 보통 bash)가 치환된 후 "4"가 붙어 "bash4"가 된다.
- 나머지 부분은 정의되지 않은 변수로 치환되어 없어지게 되므로, 최종 결과가 "abash4"가 되는 것으로 보인다.
문제는 heredoc이 기본적으로 변수 확장을 수행하기 때문에, secret 값에 포함된 $ 기호가 셸 변수로 처리되어 의도한 문자열이 깨지는 것입니다. 이 문제를 해결하려면 heredoc의 구분자를 인용부호로 감싸서 셸 확장을 막아야 합니다.
예를 들어, 아래와 같이 <<'EOF'를 사용하면 내부의 내용이 리터럴로 처리되어 secret 값이 그대로 보존됩니다:
그래서 원래는 <<EOF 였는데 작은 따옴표를 붙여서 <<'EOF' 로 바꾸니 모든게 해결됨!
그리고 혹시 몰라서 모든 변수에 다 작은 따옴표도 붙여줬다.
# 1. .env 파일 생성
cat <<'EOF' > /home/ubuntu/.env #이 부분이 바뀌었다! 그리고 변수 앞뒤로 작은 따옴표도 붙여줌.
NAVER_CLIENT_ID='${{ secrets.NAVER_CLIENT_ID }}'
NAVER_CLIENT_SECRET='${{ secrets.NAVER_CLIENT_SECRET }}'
MYSQL_URL='${{ secrets.MYSQL_URL }}'
MYSQL_USERNAME='${{ secrets.MYSQL_USERNAME }}'
MYSQL_PASSWORD='${{ secrets.MYSQL_PASSWORD }}'
CORS_ALLOWED_ORIGINS='${{ secrets.CORS_ALLOWED_ORIGINS }}'
EOF
이러면 이제 시크릿이 저절로 바뀌지 않는다!
문제 해결 완료! ㅎㅎ
그런데,, 링크드인에서 발견한 github CI 도중 secret 값 유출 사건 ... ㅜㅜ
깃허브도 안전하지 않다니.. 너무 믿고 있었나보다 ㅠㅠ
이젠 젠킨스나 AWS 시크릿 매니저 써야하나..? ㅜ
우선 나는 이전에
GitHub Actions 워크플로우(.github/workflows/*.yml) 파일의 "uses: " 부분을 그냥 @v3 이런식으로만 작성해줘서 보안에 취약했다.
[보안에 취약한 하기 코드]
uses: actions/checkout@v3
그래서 커밋 SHA로 서드파티 액션 고정을 해줘야한다!
📌 checkout@v3 릴리스 페이지에서 가장 최근 커밋 SHA를 확인하고, 최신 버전으로 고정하기!
예를 들어 현재 25년 3월의 가장 최신 버전 중 GitHub Actions용 릴리스(=버전)”에 정식으로 연결된 버전은 4.1.1로, SHA는 아래와 같다.
e33c7c86b6e46cd5793d47d47af9295cd49f582e
그래서 워크플로우에선 아래처럼 작성해주면 됨!
uses: actions/checkout@e33c7c86b6e46cd5793d47d47af9295cd49f582e
# 혹은
uses: actions/checkout@v4.1.1
이런 식으로 보안을 강화해주도록 하자..!
(그런데 나는 SHA 값으로 해도 자꾸 안돼서 그냥 @4.1.1 처럼 버전으로 써줬다.)
특히 아래 코드는 ("@master") 정말 위험하다고 한다! 공격자가 master 브랜치를 바꾸면 바로 영향받을 수 있다.
✅ 해결법: 릴리스된 커밋 SHA를 사용해 고정하기!
appleboy/ssh-action@master #매우 위험한 코드!
appleboy/ssh-action@v0.1.10 #대신 이런 식으로 버전 혹은 그 버전의 SHA 값으로 써주자!
일단 이번은 이렇게 보안하고, 다음부턴 젠킨스로 CI/CD 구축해봐야겠다.
'Computer Science > 내가 겪은 문제와 해결법' 카테고리의 다른 글
클라이언트에서 보내는 데이터를 서버에서 인식하지 못할 때 해결법! ObjectMapper 사용하기! (0) | 2024.12.31 |
---|