Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions .github/workflows/yappu-oci-dev-cd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
name: yappu-world-oci-dev-cd

on:
push:
branches:
- dev

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'liberica'
cache: gradle

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Copy Secrets
uses: microsoft/variable-substitution@v1
with:
files:
./src/main/resources/application-dev-oci.yaml
env:
server.port: ${{ secrets.DEV_SERVER_PORT }}
spring.datasource.url: ${{ secrets.DEV_OCI_DB_URL }}
spring.datasource.username: ${{ secrets.DEV_OCI_DB_USERNAME }}
spring.datasource.password: ${{ secrets.DEV_OCI_DB_PASSWORD }}
jwt.secret_key: ${{ secrets.DEV_JWT_SECRET_KEY }}
jwt.access_token_expiration_times: ${{ secrets.DEV_ACCESS_TOKEN_EXPIRATION_TIMES }}
jwt.refresh_token_expiration_times: ${{ secrets.DEV_REFRESH_TOKEN_EXPIRATION_TIMES }}
fcm.private_key_id: ${{secrets.FCM_PRIVATE_KEY_ID}}
fcm.private_key: ${{secrets.FCM_PRIVATE_KEY}}
fcm.client_email: ${{secrets.FCM_CLIENT_EMAIL}}
fcm.client_id: ${{secrets.FCM_CLIENT_ID}}
fcm.client_x509_cert_url: ${{secrets.FCM_X509_CERT_URL}}
discord.webhook: ${{secrets.DISCORD_WEBHOOK_URL_IN_DEV}}
sentry.dsn: ${{secrets.SENTRY_DSN}}
admin.domain: ${{ secrets.DEV_ADMIN_DOMAIN }}
kakao.rest_api_key: ${{ secrets.KAKAO_REST_API_KEY }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -x test -Dspring.profiles.active=dev-oci

Comment on lines +26 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the workflow file to understand the full context
cat -n .github/workflows/yappu-oci-dev-cd.yaml | head -60

Repository: YAPP-admin/yappu-world-server

Length of output: 3033


🏁 Script executed:

# Check if the application-dev-oci.yaml file exists and examine its structure
if [ -f ./src/main/resources/application-dev-oci.yaml ]; then
    echo "=== application-dev-oci.yaml content ==="
    cat ./src/main/resources/application-dev-oci.yaml
else
    echo "File not found"
fi

Repository: YAPP-admin/yappu-world-server

Length of output: 1918


🏁 Script executed:

# Search for how fcm.private_key is used in the codebase
rg "fcm\.private_key" --type yaml --type java --type properties -A 3 -B 1

Repository: YAPP-admin/yappu-world-server

Length of output: 107


🏁 Script executed:

# Look for gradle build configuration to understand what gets included in JAR
fd "build.gradle" | head -5 | xargs cat | head -100

Repository: YAPP-admin/yappu-world-server

Length of output: 3272


🌐 Web query:

microsoft variable-substitution action multiline secrets YAML parsing

💡 Result:

Short answer — microsoft/variable-substitution reads replacement values from environment variables (set in the workflow file) and those values are subject to YAML/workflow interpolation. Multiline secrets with embedded newlines often get mangled by YAML or by how you inject them into env:, so the action doesn't receive the original newline-separated content. Use one of these workarounds:

  • Store the secret base64-encoded, pass the base64 string as the env value, and in a run step decode it back to a file (echo "$SECRET_B64" | base64 --decode > file). [1][2]
  • Don’t put the multiline secret directly into env: for the action; instead in a run: step read the secret and write it with a here-doc or printf to preserve newlines (e.g. printf '%s' "$SECRET" > file). [1][3]
  • If you must use variable-substitution for YAML/JSON keys, convert multiline content to a single-line (base64 or JSON-escaped) token and reverse it at runtime as above.

References:

  • Example & discussion of multiline secrets/YAML issues in GitHub Actions. [1]
  • microsoft/variable-substitution behavior (uses env variables; YAML/JSON processing). [2]
  • StackOverflow examples showing writing PEM/multiline secrets from GitHub Actions using run steps (here-doc / printf / decoding). [3]

If you want, tell me which approach you prefer and paste a minimal example of your workflow + secret and I’ll give the exact YAML to use.


variable-substitution 액션의 멀티라인 시크릿 처리로 인한 YAML 파싱 오류 및 시크릿 노출 위험

Line 26-50의 흐름에서 다음 두 가지 문제가 확인됩니다:

  1. 멀티라인 시크릿 손상: fcm.private_key는 Firebase 서비스 계정 개인키(JSON/PEM 형식의 멀티라인 콘텐츠)인데, env: 변수를 통해 variable-substitution@v1로 전달되면서 YAML 파싱 과정에서 줄바꿈이 손상되어 키 값이 깨질 수 있습니다.

  2. 시크릿이 JAR에 포함됨: ./src/main/resources/application-dev-oci.yaml에 치환된 시크릿은 Spring Boot 빌드 과정에서 JAR의 resources 디렉토리에 그대로 포함되므로, 배포된 JAR 파일에 모든 시크릿이 평문으로 저장됩니다.

멀티라인 시크릿(특히 개인키)은 base64 인코딩 후 런타임에 디코딩하거나, run: 스텝에서 here-doc/printf를 사용해 직접 파일로 작성하는 방식으로 처리해야 합니다.

🤖 Prompt for AI Agents
.github/workflows/yappu-oci-dev-cd.yaml lines 26-51: variable-substitution is
breaking multiline secrets (fcm.private_key) and causes secrets to be embedded
into the built JAR; stop passing multiline keys through env:
variable-substitution and instead pass a base64-encoded secret or the raw secret
only at runtime and write it to a file in a run: step (use here-doc or printf to
decode/base64 -d into ./src/main/resources or an external file consumed at
startup), remove fcm.private_key (and other multiline secrets) from the env
mapping so they are not substituted into application-dev-oci.yaml at build time,
and adjust the build step to use runtime files or external secret manager (OCI
Vault) so secrets are not baked into the JAR.

- name: Prepare File for Deployment
run: |
mkdir -p deployment/dev/build/libs
cp ./docker/dockerfile-dev-oci ./docker/docker-compose-dev-oci.yaml deployment/dev/
cp -r ./build/libs/yappu-world-dev-oci.jar deployment/dev/build/libs

# Github Action 실행 서버 IP 추출
- name: Get Github Actions IP
id: ip
uses: candidob/get-runner-ip@v1.0.0

# OCI CLI 설정
- name: Setup OCI CLI
run: |
mkdir -p ~/.oci
echo "${{ secrets.OCI_CLI_KEY_CONTENT }}" > ~/.oci/key.pem
chmod 600 ~/.oci/key.pem

cat > ~/.oci/config << EOF
[DEFAULT]
user=${{ secrets.OCI_CLI_USER }}
fingerprint=${{ secrets.OCI_CLI_FINGERPRINT }}
tenancy=${{ secrets.OCI_CLI_TENANCY }}
region=${{ secrets.OCI_CLI_REGION }}
key_file=~/.oci/key.pem
EOF

chmod 600 ~/.oci/config

# OCI CLI 설치
curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh | bash -s -- --accept-all-defaults
echo "$HOME/bin" >> $GITHUB_PATH
~/bin/oci --version

# NSG에 Github Action 서버를 등록
- name: Add Github Actions IP to OCI NSG
run: |
~/bin/oci network nsg rules add \
--nsg-id ${{ secrets.OCI_DEV_NSG_OCID }} \
--security-rules '[{
"direction": "INGRESS",
"protocol": "6",
"source": "${{ steps.ip.outputs.ipv4 }}/32",
"sourceType": "CIDR_BLOCK",
"tcpOptions": {
"destinationPortRange": {
"min": 22,
"max": 22
}
},
"description": "GitHub Actions temporary access",
"isStateless": false
}]'

- name: Wait for NSG rule to propagate
run: sleep 10

- name: Setup SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.OCI_DEV_SSH_PRIVATE_KEY }}" > ~/.ssh/oci_dev_key
chmod 600 ~/.ssh/oci_dev_key

- name: Upload files to OCI Instance
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.OCI_DEV_INSTANCE_IP }}
username: ubuntu
key: ${{ secrets.OCI_DEV_SSH_PRIVATE_KEY }}
source: "deployment/dev/*"
target: "/home/ubuntu"

- name: Deploy using Docker Compose
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.OCI_DEV_INSTANCE_IP }}
username: ubuntu
key: ${{ secrets.OCI_DEV_SSH_PRIVATE_KEY }}
script: |
cd /home/ubuntu/deployment/dev
docker-compose -f docker-compose-dev-oci.yaml down
docker system prune -f
docker-compose -f docker-compose-dev-oci.yaml build --no-cache
docker-compose -f docker-compose-dev-oci.yaml up -d

- name: Remove Github Actions IP from OCI NSG
if: always()
run: |
# NSG 규칙 ID 찾기
RULE_ID=$(~/bin/oci network nsg rules list \
--nsg-id ${{ secrets.OCI_DEV_NSG_OCID }} \
--all \
--query "data[?source=='${{ steps.ip.outputs.ipv4 }}/32' && direction=='INGRESS'].id | [0]" \
--raw-output)

if [ ! -z "$RULE_ID" ] && [ "$RULE_ID" != "null" ]; then
~/bin/oci network nsg rules remove \
--nsg-id ${{ secrets.OCI_DEV_NSG_OCID }} \
--security-rule-ids "[\"$RULE_ID\"]"
fi

- name: Cleanup
if: always()
run: |
rm -f ~/.oci/key.pem
rm -f ~/.ssh/oci_dev_key

# Discord Notification
- name: CD Success Notification
uses: sarisia/actions-status-discord@v1
if: success()
with:
title: ✅ OCI 개발 환경 배포 성공 ✅
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
color: 0x00FF00
username: 페페훅

- name: CD Failure Notification
uses: sarisia/actions-status-discord@v1
if: failure()
with:
title: ❗️OCI 개발 환경 배포 실패 ❗️
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
color: 0xFF0000
username: 페페훅
189 changes: 189 additions & 0 deletions .github/workflows/yappu-oci-prod-cd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
name: yappu-world-oci-prod-cd

on:
push:
branches:
- prod

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'liberica'
cache: gradle

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Copy Secrets
uses: microsoft/variable-substitution@v1
with:
files:
./src/main/resources/application-prod-oci.yaml
env:
spring.datasource.url: ${{ secrets.PROD_OCI_DB_URL }}
spring.datasource.username: ${{ secrets.PROD_OCI_DB_USERNAME }}
spring.datasource.password: ${{ secrets.PROD_OCI_DB_PASSWORD }}
jwt.secret_key: ${{ secrets.PROD_JWT_SECRET_KEY }}
jwt.access_token_expiration_times: ${{ secrets.PROD_ACCESS_TOKEN_EXPIRATION_TIMES }}
jwt.refresh_token_expiration_times: ${{ secrets.PROD_REFRESH_TOKEN_EXPIRATION_TIMES }}
fcm.private_key_id: ${{secrets.FCM_PRIVATE_KEY_ID}}
fcm.private_key: ${{secrets.FCM_PRIVATE_KEY}}
fcm.client_email: ${{secrets.FCM_CLIENT_EMAIL}}
fcm.client_id: ${{secrets.FCM_CLIENT_ID}}
fcm.client_x509_cert_url: ${{secrets.FCM_X509_CERT_URL}}
discord.webhook: ${{secrets.DISCORD_WEBHOOK_URL_IN_PROD}}
sentry.dsn: ${{secrets.SENTRY_DSN}}
admin.domain: ${{ secrets.PROD_ADMIN_DOMAIN }}
kakao.rest_api_key: ${{ secrets.KAKAO_REST_API_KEY }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -x test -Dspring.profiles.active=prod-oci

Comment on lines +26 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

빌드 타임에 시크릿을 리소스에 주입 → JAR에 시크릿 포함(유출면적 확대) 가능
application-prod-oci.yaml에 시크릿을 치환(Line 26-47)한 뒤 빌드(Line 49)를 하면, 최종 JAR에 시크릿이 포함되는 형태가 됩니다(의도라면 OK지만, 아티팩트/서버 파일 접근만으로도 유출 가능). 운영은 보통 “런타임 환경변수/외부 시크릿 매니저”로 주입하는 쪽이 안전합니다.

가능한 대안:

  • 빌드 단계 치환 제거 + 컨테이너 런타임에 env로 주입(설정은 ${...} 유지)
  • 또는 OCI Vault/Parameter Store류로 런타임 fetch

- name: Prepare File for Deployment
run: |
mkdir -p deployment/prod/build/libs
cp ./docker/dockerfile-prod-oci ./docker/docker-compose-prod-oci.yaml deployment/prod/
cp -r ./build/libs/yappu-world-prod-oci.jar deployment/prod/build/libs

# Github Action 실행 서버 IP 추출
- name: Get Github Actions IP
id: ip
uses: candidob/get-runner-ip@v1.0.0

# OCI CLI 설정
- name: Setup OCI CLI
run: |
mkdir -p ~/.oci
echo "${{ secrets.OCI_CLI_KEY_CONTENT }}" > ~/.oci/key.pem
chmod 600 ~/.oci/key.pem

cat > ~/.oci/config << EOF
[DEFAULT]
user=${{ secrets.OCI_CLI_USER }}
fingerprint=${{ secrets.OCI_CLI_FINGERPRINT }}
tenancy=${{ secrets.OCI_CLI_TENANCY }}
region=${{ secrets.OCI_CLI_REGION }}
key_file=~/.oci/key.pem
EOF

chmod 600 ~/.oci/config

# OCI CLI 설치
curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh | bash -s -- --accept-all-defaults
echo "$HOME/bin" >> $GITHUB_PATH
~/bin/oci --version

# NSG에 Github Action 서버를 등록
- name: Add Github Actions IP to OCI NSG
run: |
~/bin/oci network nsg rules add \
--nsg-id ${{ secrets.OCI_PROD_NSG_OCID }} \
--security-rules '[{
"direction": "INGRESS",
"protocol": "6",
"source": "${{ steps.ip.outputs.ipv4 }}/32",
"sourceType": "CIDR_BLOCK",
"tcpOptions": {
"destinationPortRange": {
"min": 22,
"max": 22
}
},
"description": "GitHub Actions temporary access",
"isStateless": false
}]'

- name: Wait for NSG rule to propagate
run: sleep 10

- name: Setup SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.OCI_PROD_SSH_PRIVATE_KEY }}" > ~/.ssh/oci_prod_key
chmod 600 ~/.ssh/oci_prod_key

- name: Upload files to OCI Instance
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.OCI_PROD_INSTANCE_IP }}
username: ubuntu
key: ${{ secrets.OCI_PROD_SSH_PRIVATE_KEY }}
source: "deployment/prod/*"
target: "/home/ubuntu"

- name: Deploy using Docker Compose
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.OCI_PROD_INSTANCE_IP }}
username: ubuntu
key: ${{ secrets.OCI_PROD_SSH_PRIVATE_KEY }}
script: |
cd /home/ubuntu/deployment/prod
docker-compose -f docker-compose-prod-oci.yaml down
docker system prune -f
docker-compose -f docker-compose-prod-oci.yaml build --no-cache
docker-compose -f docker-compose-prod-oci.yaml up -d

- name: Remove Github Actions IP from OCI NSG
if: always()
run: |
# NSG 규칙 ID 찾기
RULE_ID=$(~/bin/oci network nsg rules list \
--nsg-id ${{ secrets.OCI_PROD_NSG_OCID }} \
--all \
--query "data[?source=='${{ steps.ip.outputs.ipv4 }}/32' && direction=='INGRESS'].id | [0]" \
--raw-output)

if [ ! -z "$RULE_ID" ] && [ "$RULE_ID" != "null" ]; then
~/bin/oci network nsg rules remove \
--nsg-id ${{ secrets.OCI_PROD_NSG_OCID }} \
--security-rule-ids "[\"$RULE_ID\"]"
fi

- name: Cleanup
if: always()
run: |
rm -f ~/.oci/key.pem
rm -f ~/.ssh/oci_prod_key

# Discord Notification
- name: CD Success Notification
uses: sarisia/actions-status-discord@v1
if: success()
with:
title: ✅ OCI 상용 배포 성공 ✅
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
color: 0x00FF00
username: 페페훅

- name: CD Failure Notification
uses: sarisia/actions-status-discord@v1
if: failure()
with:
title: ❗️OCI 상용 배포 실패 ❗️
webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
color: 0xFF0000
username: 페페훅

update_release:
needs: build
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Update Release
uses: release-drafter/release-drafter@v6
with:
config-name: release-drafter-config.yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
runtimeOnly("org.springframework.boot:spring-boot-docker-compose")

runtimeOnly("com.mysql:mysql-connector-j")
runtimeOnly("com.oracle.database.jdbc:ojdbc11")
runtimeOnly("com.oracle.database.security:oraclepki:23.5.0.24.07")
Comment thread
devmizz marked this conversation as resolved.
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.linecorp.kotlin-jdsl:jpql-dsl:3.5.5")
implementation("com.linecorp.kotlin-jdsl:jpql-render:3.5.5")
Expand All @@ -52,6 +54,7 @@ dependencies {

// logging
implementation("io.github.oshai:kotlin-logging-jvm:7.0.0")
implementation("com.github.loki4j:loki-logback-appender:2.0.1")

// swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
Expand Down
11 changes: 11 additions & 0 deletions docker/docker-compose-dev-oci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
app:
image: yappu-world-dev-oci
container_name: yappu-world-dev-oci
build:
context: .
dockerfile: dockerfile-dev-oci
ports:
- '8080:8080'
volumes:
- /opt/oracle/wallet:/wallet:ro
Loading
Loading