영업 뛰다가 온, 남들과 조금 다른 주니어 개발자

영업하면서 배운 고객 중심적 사고, 비즈니스 통찰력 등을 총동원해서 서비스를 개발하고 있습니다. 영업 경험이 개발하는 과정에서 큰 역할을 하더라구요, 즐겁게 개발하고 있습니다!

Computer Science/내가 겪은 문제와 해결법

Github Secret CI/CD 과정 - 시크릿키 자동 바뀜 문제 해결, 시크릿 유출 문제 대안 - 보안 강화

브윗 2025. 4. 1. 14:31

정말 찾아내기가 너무 어려웠던... 배포에서의 시크릿키 문제..

 

정말 거의 일주일 내내 힘들게 원인을 찾았고, 겨우 찾아냈다!

나중에 절대 겪지 않기 위해 작성해보는 이야기!

 

나는 React + Spring Boot 로 풀스택 개발을 진행했고, 이 분 블로그를 보고 열심히 잘 따라했다.

https://velog.io/@g6y116/6

 

[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 값 유출 사건 ... ㅜㅜ

https://www.linkedin.com/posts/blackcon_ci-dockerhub-aws-activity-7308133470675881984-aFnl?utm_source=share&utm_medium=member_desktop&rcm=ACoAACo1Q08B_3tB2H59Yjs3dczlmD8pyJzJXBU

 

깃허브도 안전하지 않다니.. 너무 믿고 있었나보다 ㅠㅠ

이젠 젠킨스나 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 릴리스

appleboy/ssh-action@master #매우 위험한 코드!

appleboy/ssh-action@v0.1.10 #대신 이런 식으로 버전 혹은 그 버전의 SHA 값으로 써주자!

 

 

일단 이번은 이렇게 보안하고, 다음부턴 젠킨스로 CI/CD 구축해봐야겠다.