Skip to content

chore(S15P11A705-154): add runtime secret handoff workflow - #100

Merged
tpals0409 merged 2 commits into
devfrom
feat/S15P11A705-154-runtime-secret-handoff
Jul 30, 2026
Merged

chore(S15P11A705-154): add runtime secret handoff workflow#100
tpals0409 merged 2 commits into
devfrom
feat/S15P11A705-154-runtime-secret-handoff

Conversation

@tpals0409

@tpals0409 tpals0409 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

요약

  • 운영 런타임 Secret 5개를 pinlog-secrets-prod Environment에서만 읽는 수동 workflow를 추가합니다.
  • SHA 고정 Infra composite action이 back-prod policy로 SealedSecret Infra Draft PR을 만들도록 연결합니다.
  • trigger·최소 권한·immutable revision·정확한 Secret 참조 집합을 정적 계약 테스트로 고정합니다.

Jira (필수)

  • S15P11A705-154

관련 GitHub Issue (선택)

  • 없음

변경 사항

  • .github/workflows/seal-runtime-secrets.yml
  • RuntimeSecretWorkflowContractTests
  • BI-25 및 WORKLOG 기록

테스트 / 검증

  • RED: ./gradlew test --tests com.pinlog.pinlogback.RuntimeSecretWorkflowContractTests --no-daemon → workflow 부재로 3개 실패
  • GREEN: 동일 명령 → 성공, exit 0
  • bracket 표기 mutation: backend-ci.yml${{ secrets['JWT_PRIVATE_KEY'] }}를 임시 주입 → 비접근 계약 테스트 실패 후 원복, GREEN 재확인
  • ./gradlew test --tests com.pinlog.pinlogback.RuntimeSecretWorkflowContractTests checkstyleTest --rerun-tasks --no-daemon → 계약 테스트 4개·checkstyle 성공, exit 0
  • actionlint .github/workflows/seal-runtime-secrets.yml (v1.7.12 checksum 검증 설치) → 성공, exit 0
  • ./gradlew clean check --no-daemon → 실행했으나 runner에 Docker socket이 없어 Testcontainers 초기화 34건 실패(158개 중 124개 통과); checkstyleMain·checkstyleTest는 성공. PR CI에서 Docker 포함 전체 gate 확인 필요
  • git diff --cached --check → 성공

리뷰 포인트

  1. 일반 backend-ci에는 runtime Secret 참조나 권한을 추가하지 않았습니다.
  2. owner Secret 5개와 PINLOG_INFRA_SECRET_PR_TOKEN bridge 1개 외 참조가 없습니다.
  3. 실제 Secret 값·Environment 설정·권한 설정은 조회하거나 변경하지 않았습니다.

미결 / 후속

  • 추가 hardening·일반화는 이 PR 범위 밖입니다.

@tpals0409
tpals0409 force-pushed the feat/S15P11A705-154-runtime-secret-handoff branch from 98587a2 to b1f2f02 Compare July 29, 2026 10:53
@tpals0409 tpals0409 changed the title chore(S15P11A705-154): 운영 Secret 전달 workflow 추가 chore(S15P11A705-154): add runtime secret handoff workflow Jul 29, 2026
@tpals0409
tpals0409 marked this pull request as ready for review July 29, 2026 10:59

@cherry-go-round cherry-go-round left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CLIENT_SECRET 외에도 CLIENT_ID도 필요합니다. 자세한 건 내일 같이 얘기하면 될 것 같습니다.

GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
KAKAO_CLIENT_ID
KAKAO_CLIENT_SECRET
NAVER_CLIENT_ID
NAVER_CLIENT_SECRET

@tpals0409

Copy link
Copy Markdown
Contributor Author

확인했습니다. 현재 PR workflow에는 아래 5개 런타임 Secret이 계약되어 있습니다.

  • JWT_PRIVATE_KEY
  • GOOGLE_CLIENT_SECRET
  • KAKAO_CLIENT_SECRET
  • NAVER_CLIENT_SECRET
  • PINLOG_AI_INTERNAL_SECRET

리뷰 내용은 기존 OAuth *_CLIENT_SECRET 3개를 유지하면서 다음 *_CLIENT_ID 3개를 추가해야 한다는 요청으로 이해했습니다.

  • GOOGLE_CLIENT_ID
  • KAKAO_CLIENT_ID
  • NAVER_CLIENT_ID

따라서 별도 제거 요청이 없다면 최종 runtime key 집합은 위 8개이고, Infra PR 생성용 bridge credential PINLOG_INFRA_SECRET_PR_TOKEN은 별도입니다. 값은 PR·채팅·Jira에 공유하지 않고 pinlog-secrets-prod Environment에서만 등록·회전하는 경계를 유지하겠습니다.

JWT_PRIVATE_KEY 또는 PINLOG_AI_INTERNAL_SECRET도 이번 계약에서 제외해야 한다면 내일 논의 때 그 두 항목만 명시해 주세요. 그 전에는 현재 Changes Requested 상태를 유지하고, 8-key allowlist/workflow/test가 일치하도록 수정한 뒤 재검증하는 것이 안전합니다.

@colosair

Copy link
Copy Markdown
Member

BI-25를 두 PR이 선점하고 있습니다

이 PR의 BI-25-2026-07-29-runtime-secret-workflow.md와, AI 파트의 #98이 만든 BI-25-2026-07-29-personal-search-backend-integration.md같은 번호입니다.

$ bash check-number.sh back:BI
⚠ 충돌  BI-25
    feat/S15P11A705-154-runtime-secret-handoff
    feat/S15P11A705-135-personal-search

파일명이 달라 git이 충돌로 잡지 않습니다. 둘 다 병합되면 WORKLOG.md(merge=union)에 두 행이 나란히 들어가고 각각 다른 파일을 BI-25로 링크합니다.

implements/README의 규칙대로 나중에 머지되는 쪽이 재배정하면 됩니다. 지금 #98CLEAN이고 이 PR은 CHANGES_REQUESTED라 순서가 갈릴 수 있어, 어느 쪽이든 머지 직전에 한 번 더 확인하시는 편이 안전합니다. 다음 미사용 번호는 BI-26입니다.

사실만 전달드립니다 — 어느 쪽이 양보할지는 머지 순서가 정하는 것이고 제가 정할 일이 아닙니다. 오늘까지 같은 형태(V4·BD-29/BI-18·BD-35/BI-21·BD-36/BI-22)가 네 번 났고 매번 머지 후에 발견돼서, 이번엔 미리 알려 둡니다.

@minyongP minyongP left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude Code 자동 리뷰입니다. 판정은 항상 comment이며, 승인·변경 요청은 @minyongP가 직접 남깁니다.

필수 변경은 없습니다. Infra 쪽 계약(84458bf 시점의 sealedsecret-infra-pr)과 하나씩 대조했고 어긋나는 곳이 없었습니다. back-prod policy가 요구하는 sourceWorkflow 경로와 이 파일 위치가 같고, env의 owner 키 5개가 policy의 ownerSecretKeys와 정확히 같고, bridge token 이름도 action 스크립트가 읽는 PINLOG_INFRA_SECRET_PR_TOKEN과 같습니다.

id-token: writecheckout(ref: github.sha)는 과한 권한이 아니라 action이 요구하는 것이었습니다 — 각각 OIDC 클레임 대조(validate_oidc_claims)와, 체크아웃된 워크스페이스에서 이 workflow가 action SHA를 정확히 한 번 고정하는지 확인하는 verify_workflow_binding이 씁니다. 최소 권한 주장이 실제로 최소였습니다.

선택 제안 둘을 줄 단위로 달았습니다. 하나는 backend-ci.yml 한 파일만 지키는 비접근 계약을 workflow 전체로 넓히는 것, 다른 하나는 이 workflow가 dev에서만 실행된다는 사실을 어딘가에 남기는 것입니다(policy의 trustedRefrefs/heads/dev라 다른 브랜치에서 dispatch하면 상대 레포 메시지로 실패합니다).

좋았던 점. Secret 참조 집합을 "정확히 이 6개"로 못 박아 둔 것이 이 workflow에서 가장 값어치 있는 부분이었습니다 — 키가 하나 늘 때 사람이 아니라 테스트가 먼저 말하게 됩니다. SnakeYAML이 on:Boolean.TRUE로 읽는 것을 주석으로 남겨 둔 것도 좋습니다.

확인 부탁. clean check가 Docker 부재로 34건 실패한 상태로 올라와 있으니 PR CI의 전체 gate 결과만 확인해 주세요.

Comment thread .github/workflows/seal-runtime-secrets.yml
@tpals0409

Copy link
Copy Markdown
Contributor Author

Infra 계약 merge 완료

Infra PR Team-PinLog/infra#82가 일반 merge commit으로 병합됐습니다.

  • canonical Infra action pin SHA: 16bfae0da4e1091df597fb89f6acf914391e11b9
  • focused contract tests: 27/27 PASS
  • Infra full regression: 219/219 PASS
  • required CI: 8/8 PASS

Backend workflow의 uses: Team-PinLog/infra/.github/actions/sealedsecret-infra-pr@...를 위 immutable merge SHA로 pin하고, owner key 집합을 다음 8개와 exact/order 일치시켜 주세요.

  1. JWT_PRIVATE_KEY
  2. GOOGLE_CLIENT_ID
  3. GOOGLE_CLIENT_SECRET
  4. KAKAO_CLIENT_ID
  5. KAKAO_CLIENT_SECRET
  6. NAVER_CLIENT_ID
  7. NAVER_CLIENT_SECRET
  8. PINLOG_AI_INTERNAL_SECRET

PINLOG_INFRA_SECRET_PR_TOKEN은 owner Secret이 아니라 bridge credential로 계속 분리합니다. Secret 값은 댓글·PR·로그에 공유하지 않습니다.

기존 BI-25 번호 충돌도 병합 전에 BI-26 등 미사용 번호로 정리해 주세요. 이 댓글은 Infra handoff 완료 사실만 전달하며 앱 변경이나 merge를 수행하지 않습니다.

@colosair

Copy link
Copy Markdown
Member

back#98 이 병합되어 BI-25 배정이 확정됐습니다. 사실만 전달합니다.

  • 병합: 1a5091e (2026-07-30T01:28:38Z)
  • dev 에 들어간 파일: docs/backend/implements/BI-25-2026-07-29-personal-search-backend-integration.md

bash .claude/scripts/check-number.sh back:BI 실측(2026-07-30 01:4x · dev + 열린 PR 스캔) 기준 다음 미사용 번호는 26 입니다.

이 PR 이 DIRTY 로 바뀐 것도 같은 병합 때문입니다. docs/backend/WORKLOG.md 에 양쪽이 한 줄씩 추가해 충돌했습니다 — 파일명이 다른 BI-25 두 개는 git 이 충돌로 잡지 못하고, 실제로 충돌한 것은 WORKLOG 쪽입니다.

처분은 그쪽 판단에 맡깁니다.

cherry-go-round added a commit that referenced this pull request Jul 30, 2026
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다.

공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고
있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서
두 가지를 함께 정리했다.

- 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는
  선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다
- 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과
  부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다

RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED)

GREEN
- V6__social_account_email_not_null.sql
- OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거
- SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거
- FlywayMigrationTests에 스키마 제약 단언 추가

드러난 것

- 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서
  SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을
  고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께
  깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다
- 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위
  테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은
  OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는
  어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다
- 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은
  공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런
  환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다

프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로
묶인다고 답하고 08 §3.2에 명시했다.

기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 #98이 먼저 머지되며 그 번호를
가져갔다. 남은 #100이 BI-26, 이 기록이 BI-27이 되도록 미리 배정했다 — 재번호를 두 번
하지 않으려는 선택이고, #100 머지 전까지 색인에 BI-26 행이 없다.

clean check 342개 통과, 실패 0

Refs #97

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tpals0409
tpals0409 merged commit 2e0ab16 into dev Jul 30, 2026
2 checks passed
@tpals0409
tpals0409 deleted the feat/S15P11A705-154-runtime-secret-handoff branch July 30, 2026 04:17
cherry-go-round added a commit that referenced this pull request Jul 30, 2026
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다.

공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고
있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서
두 가지를 함께 정리했다.

- 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는
  선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다
- 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과
  부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다

RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED)

GREEN
- V6__social_account_email_not_null.sql
- OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거
- SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거
- FlywayMigrationTests에 스키마 제약 단언 추가

드러난 것

- 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서
  SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을
  고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께
  깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다
- 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위
  테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은
  OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는
  어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다
- 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은
  공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런
  환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다

프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로
묶인다고 답하고 08 §3.2에 명시했다.

기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 머지 순서대로 확정됐다 — #98이 BI-25,
#100이 BI-26, 이 기록이 BI-27이다. #100 머지 전에 BI-27을 미리 배정해 재번호를 한 번만
했다.

clean check 378개 통과, 실패 0 (dev의 #98·#100 머지분 위에서 재실행)

Refs #97

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
minyongP pushed a commit that referenced this pull request Jul 30, 2026
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다.

공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고
있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서
두 가지를 함께 정리했다.

- 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는
  선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다
- 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과
  부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다

RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED)

GREEN
- V6__social_account_email_not_null.sql
- OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거
- SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거
- FlywayMigrationTests에 스키마 제약 단언 추가

드러난 것

- 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서
  SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을
  고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께
  깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다
- 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위
  테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은
  OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는
  어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다
- 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은
  공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런
  환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다

프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로
묶인다고 답하고 08 §3.2에 명시했다.

기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 머지 순서대로 확정됐다 — #98이 BI-25,
했다.

clean check 378개 통과, 실패 0 (dev의 #98·#100 머지분 위에서 재실행)

Refs #97

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
minyongP pushed a commit that referenced this pull request Jul 30, 2026
프론트에 이메일을 표시하는 화면이 있어 값 없는 계정을 둘 수 없다.

공용 계약이 먼저 바뀌어야 했다. 06 §2.2가 "미동의·미제공 시 null일 수 있다"로 정하고
있어 구현만 바꾸면 계약 위반이다(CLAUDE.md 9번). docs#28을 먼저 병합했고, 그 PR에서
두 가지를 함께 정리했다.

- 1차 보장은 공급자 콘솔의 필수 동의 설정이다. 사용자가 이메일만 거절하고 진행하는
  선택지가 동의 화면에 없으므로, 이 구현이 막는 것은 일상 흐름이 아니라 방어선이다
- 마스킹은 치환이며 NULL이 아니다. 적지 않으면 탈퇴 구현이 NULL을 넣어 이 제약과
  부딪힌다. provider_user_id가 이미 NOT NULL이면서 마스킹 대상이라 전제는 원래 있었다

RED — 정규화 4건 + 콜백 3건 실패(기존 계약을 반대로 고정하는 것이라 뒤집는 것 자체가 RED)

GREEN
- V6__social_account_email_not_null.sql
- OAuthUserInfo 세 분기 모두 이메일을 required(...)로 끊음, @nullable 제거
- SocialAccount @column(nullable = false), 팩토리 파라미터 @nullable 제거
- FlywayMigrationTests에 스키마 제약 단언 추가

드러난 것

- 뒤집을 테스트가 예상보다 많았다. 사전 조사로 3건을 찾았는데 clean check에서
  SocialAccountPersistenceTests가 걸렸다. emailIsOptional이 영속성 층에서 null 저장을
  고정하고 있었고, 같은 파일의 조회 테스트도 준비 코드에 null 이메일이 섞여 함께
  깨졌다 — useEmail(null)·isNull() 검색으로는 안 걸리는 형태다
- 두 방어선이 독립임을 뮤테이션이 보여 줬다. 정규화의 required(...)만 되돌리면 단위
  테스트 4건은 실패하지만 콜백 테스트 3건은 통과한다 — DB NOT NULL이 대신 잡아 같은
  OAUTH_FAILED로 귀결하기 때문이다. 콜백 테스트는 관측 가능한 계약을, 단위 테스트는
  어느 층이 막는가를 고정한다. 반대 방향은 FlywayMigrationTests가 잡는다
- 백필을 넣지 않았다. 운영 DB에 NULL 행이 없고, 있었다면 채울 값이 없다 — 이메일은
  공급자가 주는 값이다. 임의 값을 넣으면 "표시할 이메일"이라는 목적이 깨지므로 그런
  환경에서는 마이그레이션이 실패하는 편이 맞다고 보고 SQL 주석에 남겼다

프론트 질문(실패 사유를 별도 error 값으로 가르는지)에는 기존 결정대로 OAUTH_FAILED로
묶인다고 답하고 08 §3.2에 명시했다.

기록은 BI-27이다. 세 PR이 BI-25를 동시에 선점했고 머지 순서대로 확정됐다 — #98이 BI-25,
했다.

clean check 378개 통과, 실패 0 (dev의 #98·#100 머지분 위에서 재실행)

Refs #97

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants