v1.11.9
Relio v1.11.9 — 인증 없는 CSP 보고로 나던 500 제거, OpenAPI 문서를 실제 라우터와 일치
이번 릴리스는 두 가지 계약 문제를 바로잡습니다. 브라우저가 보낼 수 있는 형태의 CSP 위반 보고 하나가 서버를 패닉시키던 문제와, /api/openapi.json이 서버가 실제로 제공하지 않는 API를 설명하던 문제입니다.
1. 지시자 없는 CSP 보고가 일으키던 패닉
/api/v1/csp-report는 브라우저가 보내는 위반 보고를 받는 엔드포인트로, 로그인 이전 화면도 정책을 위반할 수 있으므로 인증이 없습니다. 이 핸들러가 구형 보고 본문에서 지시자 이름을 꺼내는 코드가 다음과 같았습니다.
directive = strings.Fields(legacy.Report.Violated + " ")[0]뒤에 붙인 공백은 가드처럼 보이지만 아무 역할도 하지 않습니다. strings.Fields는 공백뿐인 문자열에 빈 슬라이스를 돌려주므로, 첫 원소를 읽는 순간 index out of range로 패닉합니다.
effective-directive와 violated-directive는 둘 다 선택 항목입니다. blocked-uri만 담고 두 지시자가 모두 없는 보고 — 브라우저가 실제로 보낼 수 있는 형태입니다 — 가 도착하면 요청은 500으로 끝나고 로그에는 스택트레이스가 남았습니다. 인증이 없으므로 누구든 이 응답과 로그를 반복해서 유발할 수 있었습니다.
| 보고 본문 | 수정 전 | 수정 후 |
|---|---|---|
effective-directive: "script-src" |
script-src |
script-src |
violated-directive: "script-src 'self'" |
script-src |
script-src |
blocked-uri만 있고 두 지시자 모두 없음 |
패닉 → 500 |
204 |
| 두 지시자가 모두 공백 문자열 | 패닉 → 500 |
204 |
지시자 이름을 꺼내는 일을 reportedDirective 헬퍼로 분리했습니다. effective-directive를 먼저 쓰고, 없으면 정책 전체를 담는 violated-directive의 첫 토큰을 쓰며, 어느 쪽도 이름을 대지 못하면 빈 문자열을 돌려줍니다. 빈 값은 RecordViolation이 이미 버리므로 이름 없는 위반이 통계에 섞이지 않습니다.
2. OpenAPI 문서와 라우터의 불일치
/api/openapi.json은 REST·MCP 클라이언트가 가진 유일한 계약인데, 이 문서를 라우터와 대조하는 장치가 없어 서로 어긋나 있었습니다.
PUT /opportunities/{id}/playbook을 약속했지만 서버는 이 경로에405로 답합니다. 실제 라우트는 항목을 받는PUT /opportunities/{id}/playbook/{itemId}입니다- 로그인 흐름 전체(
/auth/status,/auth/login,/auth/logout,/auth/me, OIDC 시작·Callback)와/me/password,/dashboard등 8개 엔드포인트가 문서에서 빠져 있었습니다. 문서만 읽는 클라이언트는 인증하는 방법조차 알 수 없었습니다
빠진 경로를 채우고 Playbook 경로를 실제 라우트로 바로잡았습니다.
3. 재발 방지
문서가 다시 조용히 어긋나지 않도록, 패키지 원본에서 파싱한 라우트 표와 OpenAPI 문서를 양방향으로 대조하는 Test 2개를 두었습니다. 새 /api/v1 라우트는 문서에 적히기 전까지, 문서에 적힌 경로는 라우팅되기 전까지 빌드를 실패시킵니다.
검증
- CSP 보고 단위 Test 4개(두 지시자 조합)와 지시자 없는 보고가
204로 끝나는 핸들러 Test 추가 - OpenAPI 문서와 라우터를 양방향 대조하는 계약 Test 2개 추가
- Go 전체 Test/Vet (
go test ./...,go vet ./...),gofmt - TypeScript Typecheck와 React Production Build
- 환경변수 계약(
check-env-contract.sh)과 외부 정적 Asset 차단(check-static-assets.sh)
업그레이드
DB Migration은 없습니다. 저장된 데이터와 화면 동작에는 영향이 없습니다.
/api/openapi.json을 읽어 클라이언트를 생성해 두었다면 다시 생성하세요. Playbook 항목 저장 경로가 문서상 PUT /opportunities/{id}/playbook에서 PUT /opportunities/{id}/playbook/{itemId}로 바뀌지만, 서버가 답하던 경로는 처음부터 후자였으므로 실제 동작이 달라지는 것은 아닙니다.
오프라인 이미지
| 항목 | 값 |
|---|---|
| Asset | relio-v1.11.9.tar.gz |
| Docker Image | relio:v1.11.9 |
| SHA-256 | b9ff330078b0b65a02c93f8b63eeb3ca12a2658ddbfdac5f88deb7a9524dbfec |
| 압축 크기 | 36M |
| Architecture | linux/amd64 |
이 Release가 직접 제공하는 Asset은 위 Docker Image tar.gz 하나뿐입니다.
오프라인 서버에서 로드
gunzip -c relio-v1.11.9.tar.gz | docker load
docker image inspect relio:v1.11.9실행
# 최초 1회만 생성하고 비밀번호 관리 도구에 보관하세요.
ENCRYPTION_KEY="$(openssl rand -hex 32)"
docker run -d \
--name relio \
-p 8080:8080 \
-e POSTGRES_DSN="postgres://relio:password@postgres:5432/relio" \
-e BOOTSTRAP_ADMIN="admin" \
-e BOOTSTRAP_ADMIN_PASSWORD="ChangeMe-Immediately" \
-e ENCRYPTION_KEY="$ENCRYPTION_KEY" \
-v relio-data:/var/lib/relio \
relio:v1.11.9Relio Application이 받는 환경변수는 필수 3개(POSTGRES_DSN, BOOTSTRAP_ADMIN, BOOTSTRAP_ADMIN_PASSWORD)와 선택 1개(ENCRYPTION_KEY)뿐입니다.
ENCRYPTION_KEY를 설정하면 Personal Key와 SSO Client Secret이 재기동, 이미지 교체, relio-data Volume 재생성 후에도 유지됩니다. 설정하지 않으면 자격증명을 여는 Key가 Volume 안에만 존재하므로 /var/lib/relio를 PostgreSQL과 같은 복구 시점으로 반드시 보존해야 합니다.
검증
Release 전에 Source Test, TypeScript Typecheck, Frontend Build, Go Vet, 환경변수 계약, 외부 정적 Asset 차단, 비 Root 실행, 완전 격리 Network 신규 설치 Smoke Test, 이전 Release 업그레이드 Test, Docker Save/Load 재검증을 통과합니다.