# 문제상황
Spring 프로젝트를 하면서 인증 관련 코드들을 많이 수정했습니다. 원래 의도했던 방식은 OAuth 인증과 JWT 인증을 잘 분리해서 JWT 인증 부분만 테스트에 적절하게 적용시키는 것이었으나 Bean 관리가 아직 서툴러서 JWT 부분만 딱 떼어낼 수 없었습니다. (많이 노력하다 결국 실패했지만 추측컨대 분명 가능한 방식이라 생각합니다.) 그렇다보니 Oauth 인증에 필요한 설정값들이 test/application.yml에 포함되어야 하는 상황이 발생했습니다.
이 상황에서 2가지 문제가 발생했습니다.
- OAuth 설정값들을 test/application.yml에 평문으로 저장해서는 안되기에 뭔가 암호화하거나 숨겨야만 했습니다.
- 프로젝트 내에서 PR 또는 PR에 쌓이는 커밋이 생성될때 마다 전체 코드의 BUILD 성공 여부를 확인하는 Github workflow가 트리거되도록 되어있었는데 test/application.yml을 무작정 암호화시키면 Github workflow가 제대로 동작할 수 없기 때문에 Github는 값을 알도록 해야했습니다.
name: Gradle Build
on:
pull_request:
branches: [dev, master, feature/*, refactor/* ]
push:
branches: [feature/*]
jobs:
build:
runs-on: ubuntu-latest
environment: build_gradle
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew clean build
env:
IBAS_DEV_JWT_SECRET_KEY: ${{ secrets.IBAS_DEV_JWT_SECRET_KEY }}
IBAS_DEV_KAKAO_CLIENT_ID: ${{ secrets.IBAS_DEV_KAKAO_CLIENT_ID }}
IBAS_DEV_KAKAO_CLIENT_SECRET: ${{ secrets.IBAS_DEV_KAKAO_CLIENT_SECRET }}
IBAS_DEV_NAVER_CLIENT_ID: ${{ secrets.IBAS_DEV_NAVER_CLIENT_ID }}
IBAS_DEV_NAVER_CLIENT_SECRET: ${{ secrets.IBAS_DEV_NAVER_CLIENT_SECRET }}
IBAS_DEV_GOOGLE_CLIENT_ID: ${{ secrets.IBAS_DEV_GOOGLE_CLIENT_ID }}
IBAS_DEV_GOOGLE_CLIENT_SECRET: ${{ secrets.IBAS_DEV_GOOGLE_CLIENT_SECRET }}
문제들을 해결하기위해 github actions의 secrets를 설정해서 build시에 secrets를 불러오고 이 값들을 applicaiton.yml에 입력해서 실행하도록 하였습니다.
너무나 그럴듯한 방법이었기에 당연히 성공할 줄 알았으나 수많은 시도를 하고 코드를 바꿔봐도 의도대로 코드가 돌아가지 않았습니다. 더 이상했던건 Fork 했던 레포지토리에서는 아주 잘 동작한다는 것이었습니다.
코드한 줄 틀린게 없는데 왜 내 레포지토리에서는 되는데 Organization 레포지토리에서는 돌아가지 않는가에 대해 고민하다가 로그를 한줄한줄 뜯어보았습니다.
분명 permissions packages:write 권한을 부여했는데 read 권한이 자동적으로 부여되어있었고 secrets 값들을 전혀 잃어오지 못하고 있었습니다. 레포지토리의 모든 권한도 동일하게 설정을 다시했고 environment secrets로 설정되어 있던걸 respository secrets로 변경도 해보았지만 여전히 동일하게 실패했습니다. 결국 Github actions 공식문서까지 뒤져야 했습니다.
Workflows in forked repositories
Workflows don't run in forked repositories by default. You must enable GitHub Actions in the Actions tab of the forked repository.
With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository. The GITHUB_TOKEN has read-only permissions in pull requests from forked repositories.
문제는 Github 자체에서 설정된 권한제한 때문이었습니다. fork된 레포지토리로부터 발생된 workflow는 write 권한을 얻을 수 없도록 설정되어 있으며 오직 read 권한만 갖습니다. 즉, PR이 merge되어서야 확인이 가능합니다. 이 설정 때문에 내가 만든 내 레포지토리에서는 write 권한을 얻을 수 있었지만 fork된 레포지토리에서 발생한 PR에 대한 workflow는 자동적으로 read 권한만 얻게되어 secrets 값을 불러오지 못했던 것입니다.
하지만 제 입장에선 너무 말이 안된다고 생각했습니다. spring 코드는 기본적으로 build를 하면 test 코드가 같이 실행되고 모두 통과해야만 성공을 하는데(물론 다르게도 설정가능합니다.) merge가 된 이후에 workflow가 동작한다면, merge이후에야 문제를 알아차릴수 있는것이 아닌가라는 의문이 들었습니다. 안전한 merge를 위해 만든 것이 merge 이후에야 알 수 있다면 너무 위험부담이 커보였습니다. 그래서 다른 방법이 정말 없는건가 하며 찾아보았습니다.
# 해결 방법
방법은 딱 한가지 있었습니다. pull_reqeust 대신 pull_request_target을 이용하는 것입니다.
For workflows that are triggered by the pull_request_target event, the GITHUB_TOKEN is granted read/write repository permission unless the permissions key is specified and the workflow can access secrets, even when it is triggered from a fork. Although the workflow runs in the context of the base of the pull request, you should make sure that you do not check out, build, or run untrusted code from the pull request with this event. Additionally, any caches share the same scope as the base branch. To help prevent cache poisoning, you should not save the cache if there is a possibility that the cache contents were altered
GITHUB_TOKEN이 read/write 권한을 가지게 됩니다.
name: Gradle Build
on:
pull_request_target:
types: [ opened, reopened, synchronize ]
jobs:
build:
runs-on: ubuntu-latest
environment: build_gradle
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew clean build
env:
IBAS_DEV_JWT_SECRET_KEY: ${{ secrets.IBAS_DEV_JWT_SECRET_KEY }}
IBAS_DEV_KAKAO_CLIENT_ID: ${{ secrets.IBAS_DEV_KAKAO_CLIENT_ID }}
IBAS_DEV_KAKAO_CLIENT_SECRET: ${{ secrets.IBAS_DEV_KAKAO_CLIENT_SECRET }}
IBAS_DEV_NAVER_CLIENT_ID: ${{ secrets.IBAS_DEV_NAVER_CLIENT_ID }}
IBAS_DEV_NAVER_CLIENT_SECRET: ${{ secrets.IBAS_DEV_NAVER_CLIENT_SECRET }}
IBAS_DEV_GOOGLE_CLIENT_ID: ${{ secrets.IBAS_DEV_GOOGLE_CLIENT_ID }}
IBAS_DEV_GOOGLE_CLIENT_SECRET: ${{ secrets.IBAS_DEV_GOOGLE_CLIENT_SECRET }}
하지만 GITHUB에서 이렇게 강제적으로 권한을 막은 이유가 있습니다. pull_request_target은 secrets에 접근할 수 있지만 이로 인해 심각한 보안 위험이 발생할 수 있습니다.
- secrets 노출 - 악의적인 PR이 민감한 정보에 접근할 수 있게 됩니다.
- Remote Code execution - PR에 포함된 악의적인 코드들이 실행될 수 있습니다. 이는 가장 위험한 보안 문제입니다.
- Cache poisoning - 캐시는 기본 브랜치와 동일한 범위를 공유합니다. 악의적인 PR이 캐시에 영향을 주어, 이후 빌드나 배포에 문제를 일으킬 수 있습니다.
secure coding 방법은 https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 를 참고하시면 됩니다.
'Back-end' 카테고리의 다른 글
[Effective Java] 1. 객체 생성과 파괴 (2) | 2024.06.03 |
---|---|
Java Serializable/Deserializable (1) | 2024.05.02 |
[JVM 동작원리] 3. Execution engine (0) | 2023.10.16 |
[JVM 동작원리] 2. Runtime Data Area (0) | 2023.10.12 |
[JVM의 동작원리] 1. 클래스 로더 (0) | 2023.10.10 |