Skip to content

Releases: hkjang/ReSSO

ReSSO v0.9.82

Choose a tag to compare

@hkjang hkjang released this 12 Sep 22:35

로그인 화면과 개인 화면에 방문 추적 스크립트를 붙일 수 있습니다 — 콘텐츠 보안 정책을 풀지 않고. ReSSO의 모든 응답은 default-src 'self'로 잠겨 있어 지금까지는 수집 스니펫을 붙이면 브라우저가 조용히 막고 화면은 비어 보였습니다. 새 방문 추적 화면(서비스 관리자만)에서 설정하며, 기본값은 꺼짐이라 업그레이드한 곳에서는 켜기 전까지 아무것도 달라지지 않습니다.

추가

  • 수집 도구는 다섯 가지입니다. Momento · Google Analytics 4 · Google Tag Manager · Matomo · 직접 붙여넣기(8KB까지). Momento가 첫 자리인 이유는 사내 자체 호스팅 수집기라 데이터가 밖으로 나가지 않는 유일한 선택지이기 때문입니다.
  • Momento는 기본으로 같은 오리진 프록시를 씁니다. ReSSO가 /momento/*를 수집기로 넘기므로 브라우저에서 보면 스크립트와 수집 요청이 모두 ReSSO 주소이고, 정책에 외부 출처가 아예 등장하지 않습니다. 넘길 때 세션 쿠키와 Authorization은 떼어 내고, 수집기가 답한 Set-Cookie는 브라우저에 전달하지 않습니다. Reverse Proxy 뒤라면 /momento도 변경 없이 전달해야 합니다.
  • 설정은 환경 변수가 아니라 데이터베이스에 있습니다. 수집기 주소는 설치마다 다르고 서비스가 도는 중에 바뀌는데, 재배포가 필요한 설정은 꺼진 채로 남기 때문입니다. 변경은 감사 이벤트 TRACKING_UPDATE로 남습니다.

보안

  • 'unsafe-inline'은 어떤 경우에도 정책에 더하지 않습니다. 추적이 켜진 화면의 문서에만 요청마다 다른 nonce를 만들어 스니펫의 모든 <script>에 붙이고 같은 nonce를 script-src에 넣습니다. 그 문서에서도 nonce 없는 인라인 스크립트는 여전히 막힙니다.
  • 넓어지는 것은 그 문서 하나뿐입니다. /api/*·/realms/*·/mcp·/health/*·/metrics 같은 비화면 경로와 추적이 꺼진 화면의 문서는 원래의 좁은 정책을 글자 그대로 받습니다. 관리 콘솔(/admin)은 따로 켜지 않는 한 붙이지 않습니다. 추적을 끄면 정책도 즉시 원래대로 돌아갑니다.
  • 막힌 것을 화면이 말해 줍니다. 추적이 켜진 동안 report-uri가 붙어 브라우저가 막은 요청을 신고하고, 정책이 차단한 출처 표가 출처와 지시어를 보여 주며 허용 한 번으로 추가 허용 출처에 들어갑니다. 스니펫 안의 http(s) 출처는 자동으로 읽어 정책에 더합니다. 이 기록은 인스턴스별 메모리에 서로 다른 출처 100개까지만 남습니다 — 횟수가 아니라 출처가 중요하기 때문이고, 여러 인스턴스 뒤에서는 신고를 받은 인스턴스가 보이는 것만 나타납니다.
  • 로그인 화면 URL에는 일회용 요청 토큰이 붙어 있습니다. Momento는 query와 fragment를 떼고 보내지만 외부 도구는 페이지 URL을 그대로 보낼 수 있습니다. Momento + 프록시가 기본 권장 구성인 이유입니다.

스키마

  • platform_settings 테이블이 생깁니다(마이그레이션 016). Realm이 아니라 설치 전체에 속하는 설정을 키 하나에 문서 하나로 두어, 다음 설정은 새 키이지 새 마이그레이션이 아닙니다. 행이 없으면 기본값이므로 업그레이드는 아무것도 바꾸지 않습니다.

확인

  • 정책 쪽 Go 테스트 15건: 꺼져 있을 때 모든 경로가 기본 정책을 받는지, 모든 <script>에 nonce가 붙는지, 프록시일 때 외부 출처가 정책에 없는지, 신고가 기록되고 항상 204로 답하는지, 기록이 100개에서 멈추는지, 그리고 실제 문서를 띄워 스니펫이 좁은 정책 아래서 도는지(연동)까지입니다. 콘솔 쪽 4건은 기본값 표시·Momento 저장·8KB 초과 거절·차단 출처 허용입니다.
  • 관리자 가이드 5-12절에 설정 항목과 CSP와의 관계를 적었습니다.

Docker image: resso:v0.9.82

Archive: resso-v0.9.82.tar.gz

SHA-256: 6cba86acb00642aeaa63b784867a45cb9f4b733aad35755cfff64b3aeff41fa5

ReSSO v0.9.81

Choose a tag to compare

@hkjang hkjang released this 12 Sep 10:10

v0.9.80과 같은 내용이고, 오프라인 이미지가 붙은 릴리즈입니다. v0.9.80은 콘솔 테스트 하나가 받아 두지 않은 Promise 거절을 남겨 테스트 147개가 모두 통과했는데도 CI가 종료 코드 1로 떨어졌고, 그래서 이미지가 만들어지지 않았습니다. 폐쇄망 반입은 v0.9.81을 받으시면 됩니다.

  • 서비스 동작은 v0.9.80과 한 줄도 다르지 않습니다. 고친 것은 테스트 파일 하나입니다.
  • 원인: 요청을 띄워 두고 화면을 조작한 뒤에야 거절을 받을 쪽을 붙이면, 그 사이에 도착한 거절이 아무도 받지 않은 상태가 됩니다. 거절을 받을 쪽을 먼저 붙이도록 고쳤습니다.

Docker image: resso:v0.9.81

Archive: resso-v0.9.81.tar.gz

SHA-256: daabf38c36bfb4a2a05600f067a849bce8d4245b2a7852079bedd1fd071c91cd

ReSSO v0.9.80

Choose a tag to compare

@hkjang hkjang released this 12 Sep 10:03

접근 권한을 내주는 작업에는 비밀번호를 다시 묻습니다. 관리 콘솔 세션은 쿠키를 가진 사람이 곧 관리자인 자격 증명입니다. 대시보드를 읽는 데는 타당한 거래지만, 비밀번호 재설정·Role 부여·Secret 회전·API 키 발급처럼 세션보다 오래 사는 접근을 내주는 작업에는 그렇지 않습니다.

보안

  • 아홉 개 작업이 직전 5분 안의 비밀번호 확인을 요구합니다. Realm 생성과 설정 변경, 사용자 비밀번호 재설정, Role 매핑 교체, Client Secret 회전, 서명 키 회전, LDAP 공급자 삭제, 개인 API 키 생성과 회전입니다. 읽기와 나머지 변경 작업은 묻지 않습니다 — 모든 곳에서 물으면 창을 읽지 않고 넘기게 되고, 그러면 묻는 일 자체가 의미를 잃습니다.
  • 콘솔은 한 번만 묻고 하던 일을 그대로 잇습니다. 거절을 화면마다 처리하면 아홉 곳에 같은 대화 상자가 생기고 열 번째가 빠집니다. API 계층이 거절을 받아 묻고, 요청했던 작업을 그대로 다시 보냅니다. 여러 요청이 동시에 거절당해도 창은 하나입니다 — 일괄 작업이 열네 번 거절당했다고 대화 상자를 열네 개 띄우는 것은 보안 장치가 아닙니다.
  • 주는 것은 시간창이지 수명이 아닙니다. 비밀번호를 확인해도 세션은 갱신·교체되지 않고 만료 시각도 그대로입니다. 비밀번호를 다시 쳐서 세션을 무한히 연장할 수 없습니다.
  • 실패는 REAUTHENTICATION 이벤트로 남습니다. 탈취된 콘솔 세션을 두드리는 흔적이 로그인 화면의 일상적인 실패들 사이에 묻히지 않습니다. 로그인과 같은 IP·계정 제한과 계정 잠금이 그대로 적용됩니다.
  • API로 직접 호출하면 403 + reauthentication_required로 거절되고, POST /api/v1/auth/reauthenticate로 확인한 뒤 같은 요청을 다시 보내면 됩니다. OpenAPI 문서의 해당 operation에 이 응답이 기재됩니다 — 문서는 라우터가 쓰는 것과 같은 목록에서 생성되므로 둘이 어긋날 수 없습니다.

스키마

  • sso_sessions.authenticated_at이 생깁니다(마이그레이션 015). 세션이 언제 시작됐는지언제 마지막으로 본인을 증명했는지는 지금까지 같은 사실이어서 auth_timemax_agecreated_at으로 답했는데, 더는 같은 사실이 아닙니다. 묻는 쪽은 전부 이 컬럼을 읽습니다.
  • 업그레이드는 created_at에서 값을 채웁니다 — 열려 있던 세션에 하지도 않은 증명을 주지 않습니다. 즉 업그레이드 직후 auth_timemax_age 동작은 이전과 동일합니다.

확인

  • 라우터를 순회해 목록에 있는 아홉 개는 오래된 세션을 반드시 거절하고, 목록에 없는 나머지는 반드시 통과하는지 양쪽으로 검사합니다. 목록의 오타도 잡습니다. 가드를 지우는 것, 엉뚱한 곳으로 번지는 것, 만료를 없애는 것 — 셋 다 망가뜨려 실패를 확인했습니다.
  • 콘솔 쪽 6건(재시도·취소·오입력·동시 요청·다른 거절과의 구별)도 각각 망가뜨려 확인했습니다.

ReSSO v0.9.79

Choose a tag to compare

@hkjang hkjang released this 12 Sep 04:24

관리 콘솔이 키보드로 돌아갑니다. Ctrl+K 팔레트에서 방향키와 Enter가 동작하고, Realm을 이름으로 검색할 수 있으며, 세션 여러 개를 한 번에 종료할 수 있습니다. 서버 동작은 달라지지 않습니다.

콘솔

  • 팔레트를 손으로 누르지 않아도 됩니다. Ctrl+K로 열고 나면 결국 마우스를 잡아야 했는데, 그러면 메뉴를 직접 누르는 것과 다르지 않습니다. ↑↓로 옮기고 Enter로 실행하며, 포커스는 입력란에 머물러 계속 타이핑해 목록을 좁힐 수 있습니다. 활성 항목은 aria-activedescendant로 알려 스크린 리더에도 전달됩니다. Home/End는 양 끝으로 갑니다.
  • 찾지 못했으면 찾지 못했다고 말합니다. 빈 상자는 "검색 중"과 "없음"을 구별해 주지 않았습니다. 서버 검색이 진행 중이면 그렇게 표시하고, 두 글자부터 시작한다는 점도 알려줍니다.
  • 명령도 실행합니다. 프로필 메뉴를 열어야 닿던 로그아웃과 관리/개인 화면 전환을 이름으로 찾아 바로 실행합니다.
  • 아무것도 입력하지 않으면 최근에 간 화면을 먼저 보여줍니다. 매일 같은 두세 화면을 오가는데 고정된 메뉴 순서를 매번 읽고 내려갈 이유가 없습니다. 이 브라우저에만 남고, 권한이 줄어 더는 갈 수 없게 된 화면은 내놓지 않습니다.
  • Realm을 이름으로 검색합니다. Realm이 셋일 때는 드롭다운으로 충분하지만 쉰이면 굴려서 찾아야 했고, 운영자가 기억하는 표시 이름으로는 좁힐 수조차 없었습니다. 이제 표시 이름과 기술 이름 어느 쪽으로도 좁혀지고, 목록은 둘을 함께 보여줍니다 — issuer URL과 감사 기록에 나오는 것은 기술 이름 쪽입니다.
  • 세션을 골라 한 번에 종료합니다. 침해된 계정의 세션 열네 개를 대화 상자 열네 번으로 끝내던 일입니다. 행을 골라 한 번에 종료하고, 일부만 실패하면 몇 건이 남았는지 말합니다 — 열넷 중 셋이 남았는데 "완료"라고 하면 운영자는 끝나지 않은 세션을 끝난 것으로 믿고 화면을 떠납니다.
  • 이미 끝난 세션은 고를 수 없고, Realm이나 검색어가 바뀌면 선택이 풀립니다 — 화면에서 사라진 식별자에 대고 실행하는 것이 아무도 의도하지 않은 세션을 끝내는 경로입니다. 확인 대화 상자는 범위가 지금 화면에 보이는 것 중 고른 것임을 글자로 말합니다.
  • 팔레트 9건, Realm 선택 5건, 세션 일괄 종료 5건, 부분 실패 보고 8건, 최근 방문 6건의 회귀 테스트를 붙였고, 각각 해당 코드를 망가뜨려 실패하는 것을 확인했습니다.

Docker image: resso:v0.9.79

Archive: resso-v0.9.79.tar.gz

SHA-256: 8afd02ab1a9d35dd83224cc4c42ed73231be57d62c94e5eff1c9fa9dc85a74a7

ReSSO v0.9.78

Choose a tag to compare

@hkjang hkjang released this 10 Sep 14:57

이 저장소에는 화면을 쓰는 사람을 위한 문서가 한 줄도, 화면 캡처가 한 장도 없었습니다. docs/operations.md·compatibility.md·user-federation.md가 있었지만 셋 다 이미 아는 사람이 찾아 읽는 참고서입니다 — 키 회전 절차, OIDC 사양 준수 범위, LDAP 속성 매핑. 처음 로그인 화면 앞에 앉은 사람도, ReSSO를 처음 설치하는 사람도 읽을 것이 README뿐이었고, README는 개발·연동 참고 문서라 어느 화면의 어느 버튼인지는 어디에도 없었습니다.

추가

  • docs/USER_GUIDE.md(사용자 가이드)와 docs/ADMIN_GUIDE.md(관리자 가이드), 그리고 두 PDF를 더했습니다. 사용자 가이드는 로그인·비밀번호 변경·내 세션 끊기·개인 API 키까지, 관리자 가이드는 오프라인 설치·환경 변수·Realm과 Client 등록·User Federation·승인 절차·감사 로그·백업과 되돌리기까지를 다룹니다.
  • 화면 캡처 19장은 그린 것이 아니라 실제로 띄워 찍은 것입니다. v0.9.77 바이너리를 빈 PostgreSQL 위에 올리고, 새 스크립트 scripts/guide-screenshots.mjs가 데모 데이터를 REST API로 심은 뒤 headless Chrome을 CDP로 몰아 1440x900에서 찍습니다(외부 의존성 없이 Node 22의 내장 WebSocket으로 붙습니다).
  • 문서에 적힌 사실은 모두 코드에서 읽었습니다. 환경 변수 표는 internal/config가 실제로 읽는 전부이고, 안내한 Endpoint는 메서드까지 라우트 등록 자리에서 확인했으며(/metrics는 GET이지만 관리 권한이 필요하고 /mcp는 POST 전용입니다), 「막혔을 때」 표의 문구는 LoginPage.tsxauth.go에 있는 그대로입니다. 권한 모델도 마찬가지입니다 — 서비스 관리자는 API로도 화면으로도 부여할 수 없고(BOOTSTRAP_ADMIN 최초 생성과 admin recover만 세웁니다), Realm 관리자는 realm-admin Role입니다.
  • 캡처 스크립트는 남의 배포에 닿지 않습니다. 대상은 다른 스크립트와 공유하지 않는 전용 변수 RESSO_GUIDE_URL로만 받고 기본값이 없으며, loopback이 아니면 그 자리에서 멈춥니다 — 이 스크립트는 Realm·사용자·Client·API 키·승인 요청을 만들어 넣으므로 잘못 겨누면 감사 트레일에 그대로 남습니다. 전역 설정을 덮어쓰지 않고 새 객체만 만들며, 유일하게 건드리는 기존 레코드는 방금 만든 Realm의 승인 절차 스위치 하나입니다.
  • 화면에 실명·실제 주소·실제 비밀값은 없습니다홍길동/hong.gildong@example.com, 데모 회사, sso.example.com, ldaps://ad.example.com:636이고, API 키 Prefix는 버릴 인스턴스에서 방금 발급된 값입니다.
  • 정본은 하나입니다. README 맨 위에 두 문서(+PDF) 표를 더해 README가 그 요약이 아니라 개발·연동 참고용임을 밝혔고, 두 가이드는 operations.md·compatibility.md·user-federation.md가리키기만 하고 겹쳐 쓰지 않습니다.
  • 코드는 한 줄도 바뀌지 않았습니다. 이번 릴리즈가 담은 것은 문서와 캡처, 그리고 그 캡처를 만드는 스크립트뿐입니다.

Upgrade notes

동작이 달라지는 곳은 없습니다. API도, 화면도, 상태 코드도, 저장되는 데이터도 v0.9.77과 같고 마이그레이션도 설정 변경도 없습니다. 새 이미지를 받지 않아도 두 가이드는 저장소에서 그대로 읽을 수 있습니다. 두 가이드는 v0.9.77 화면을 찍은 것이라 그렇게 적혀 있는데, 이번 릴리즈가 화면을 바꾸지 않았으므로 v0.9.78에서도 그대로 맞습니다. 지원 창구와 신규 관리자에게는 README 대신 docs/USER_GUIDE.md·docs/ADMIN_GUIDE.md(또는 같은 이름의 PDF)를 안내하시면 됩니다.


Docker image: resso:v0.9.78

Archive: resso-v0.9.78.tar.gz

SHA-256: e2f41ed72bc41fb4a557b656604942484876de3b0c15826f1db376473d123a67

ReSSO v0.9.77

Choose a tag to compare

@hkjang hkjang released this 10 Sep 09:54

로그인 화면이 일어날 수 없는 계정 잠금을 경고했습니다. 이 화면은 세 번 거절되면 "여러 번 실패했습니다. 반복 실패하면 계정이 일정 시간 잠기며, 잠긴 동안에는 올바른 비밀번호로도 로그인할 수 없습니다. 비밀번호가 확실하지 않으면 잠기기 전에 서비스 관리자에게 문의하세요."를 띄웁니다. 이것은 설명이 아니라 사람이 행동으로 옮기는 지시입니다 — 비밀번호를 의심하고 관리자에게 전화하라는 것입니다. 카운터를 브라우저에 두는 것은 의도입니다(서버가 계정 존재 여부도 잠금 여부도 밝히지 않게 하려고). 그런데 거절의 종류를 보지 않고 모든 실패한 제출에 올렸고, 실패한 시도인 답은 그중 한 종류뿐입니다.

403 account_mismatch는 로그인이 성공한 경우입니다. 비밀번호는 통과했고 세션은 만들어져 쿠키까지 브라우저에 들어갔으며, 서버는 그 직전에 ResetLoginRateLimit으로 그 계정의 실패 횟수를 0으로 만든 뒤 RP가 지목하지 않은 계정에 인가 코드만 내주지 않았습니다(v0.9.73). 그래서 지정된 계정이 아닌 계정으로 세 번 로그인하면 맞았던 비밀번호를 의심하고, 잠길 수 없는 계정 때문에 관리자에게 문의하라는 안내가 떴습니다 — 그 안내가 설명하는 카운터를 방금의 시도들이 매번 초기화했으므로 잠길 일은 영구히 없습니다.

수정

  • 이제 401만 셉니다. 틀린 비밀번호·이미 잠긴 계정·비활성 계정 — 자격증명을 실제로 대조해서 나온 답은 이 셋뿐이고, 서버는 그 셋을 답하기 전에 계정에 실패로 기록합니다. 그러니 화면의 카운터가 세는 것과 잠금 정책이 세는 것이 이제 같습니다.
  • 500과 응답 없음(status 0)도 더는 세지 않습니다. 그 둘은 이쪽이 시도를 끝내지 못한 것이고 계정에는 아무것도 기록되지 않습니다. 저장소 장애를 만난 사람에게 비밀번호를 의심하라는 안내가 따라붙던 것이 사라집니다.
  • 429도 세지 않습니다. 그 답에는 Retry-After가 실려 있고 화면은 이미 그것으로 언제 다시 시도할 수 있는지 정확한 초를 세어 보여줍니다 — 정책을 일반론으로 경고하는 것보다 정확한 답이 같은 자리에 이미 있습니다.
  • account_mismatch가 붉은 줄 하나를 틀린 비밀번호와 공유하던 것도 고쳤습니다. 그렇게 보이면 "안 됐다"로 읽히는데, 눈앞의 폼이 아직 출구라는 말이 아무 데도 없었습니다. 인가 요청은 일부러 소진되지 않으므로 같은 화면에서 지정된 계정으로 다시 로그인하면 흐름이 그대로 이어지고, RP로 돌아가는 것은 같은 대조에 걸릴 request token을 새로 받는 일일 뿐입니다. 이제 제목과 함께 "연결한 서비스로 돌아가지 마세요. 이 화면에서 요청한 계정으로 다시 로그인하면 그대로 이어집니다."를 말하며, 계정 이름은 여전히 밝히지 않습니다.
  • 백엔드는 손대지 않았습니다. 서버는 이미 세 가지를 구별해 답하고 있었고, 그 구별을 버린 것은 화면입니다.
  • 새 테스트 둘이 더는 세지 않는 두 답으로 각각 세 번 제출해 잠금 안내가 뜨지 않는 것, account_mismatch가 "이 화면에서 다시 로그인하면 이어진다"고 말하는 것, 폼이 계속 쓸 수 있는 것을 지킵니다. 수정 전 페이지에서 둘 다 실제로 실패하는 것을 확인했습니다(잠금 안내 Alert이 그대로 떴습니다). 틀린 비밀번호에 안내가 유지되는 것은 기존 테스트가 지킵니다.
  • docs/operations.md의 계정 잠금 절에서 "잠금 안내를 봤다는 문의가 곧 잠김을 뜻하지는 않는다"를 이제는 뜻한다로 바꿔 적고, 안내를 만들지 않는 두 답을 나열했습니다. docs/compatibility.mdid_token_hint 행에는 화면이 그렇게 안내한다는 것을 적었습니다.

Upgrade notes

동작이 달라지는 곳은 로그인 화면 하나입니다: 비밀번호 실패가 아닌 거절(403 account_mismatch, 500, 응답 없음, 429)이 이제 잠금 경고를 만들지 않고, account_mismatch는 붉은 오류 대신 경고와 다음에 할 일로 보입니다. 틀린 비밀번호에서는 아무것도 달라지지 않습니다 — 세 번째 거절에 같은 안내가 같은 문구로 뜹니다. API도, 상태 코드도, 저장되는 데이터도 그대로이고 마이그레이션도 설정 변경도 없습니다. 지원 창구에는 이 안내를 봤다는 문의가 이제 실제로 잠금 정책이 걸렸다는 뜻임을 알리시면 됩니다(docs/operations.md). 되돌릴 때 이전 이미지도 그대로 동작합니다.


Docker image: resso:v0.9.77

Archive: resso-v0.9.77.tar.gz

SHA-256: 1bf06681ecbe7699dd53b4fd3d05f20512cb211447c1910eeee7299bad22e22a

ReSSO v0.9.76

Choose a tag to compare

@hkjang hkjang released this 10 Sep 01:11

로그인 화면이 이쪽 장애를 "로그인 요청이 만료되었다"고 답했습니다. RP에서 넘어온 로그인 화면은 폼을 그리기 전에 GET /api/v1/auth/challenge/{token}으로 park된 인가 요청을 먼저 읽는데, 그 읽기의 모든 실패에 한 문장으로 답했습니다 — "로그인 요청이 만료되었습니다. 연결한 서비스에서 다시 시작하세요." 그 답이 맞는 경우는 하나뿐입니다. 서비스는 소진·만료·발급된 적 없는 request token에만 404를 답하고, 그것만이 토큰에 관한 사실입니다. 저장소가 답하지 않으면 500이고, 페이지가 재시작 중에 fetch하면 응답 자체가 없습니다 — 그 둘은 이쪽 문제이고, 사람이 들고 있는 요청은 소진되지 않은 채 그대로 남아 있습니다. 그래서 그 한 문장은 설명이 아니라 지시였고, 되는 쪽에서 사람을 떼어놓았습니다: RP로 돌아가 새 request token을 받고, 여기 도착해 같은 장애를 다시 만납니다.

수정

  • 이제 404만 옛 문구를 유지합니다. 그 경우에만 연결한 서비스에서 다시 시작하는 것이 유일한 출구이기 때문입니다.
  • 나머지 실패는 "요청은 그대로 남아 있으니 연결한 서비스에서 다시 시작하지 말고, 잠시 후 다시 시도하세요"로 답합니다. 같은 challenge를 다시 부르는 다시 시도 버튼과, 문의 전화에 쓸 Trace ID가 함께 나옵니다.
  • 그 Alert이 이 화면의 마지막 말이었습니다 — 누를 것이 없고 challenge.isError가 폼을 계속 막으므로, 장애가 걷힌 뒤에도 화면은 글자 그대로 같아 보였습니다. 이제 다시 시도가 성공하면 같은 요청으로 그 자리에서 흐름이 이어집니다.
  • 근거는 한 디렉터리 옆에 이미 있었습니다. 콘솔의 ErrorAlert(components/Feedback.tsx)는 status 0·429·5xx에만 재시도를 내주고 나머지에는 내주지 않으며, 각 상태에 무엇을 할지까지 적어 줍니다. 로그인하지 않은 사람이 보는 유일한 화면만 그 구분을 버리고 있었습니다.
  • 백엔드는 손대지 않았습니다. 이 구분은 writeStoreError가 이미 하고 있었습니다 — 404와 500을 나누는 판단은 서버에 있었고, 그것을 버린 것은 화면이었습니다.
  • 새 테스트 둘이 지킵니다: 소진된 토큰(404)은 여전히 RP로 돌려보내고 재시도 버튼을 내주지 않는 것과, 500은 다른 문구와 Trace ID를 보이고 만료 문구를 보이지 않으며, 재시도 뒤 Client 안내가 뜨고 폼이 다시 쓸 수 있게 되는 것. 두 번째가 수정 전 페이지에서 실제로 실패하는 것을 확인했습니다.
  • docs/operations.md에 두 문구를 원인과 사용자가 할 일로 나눠 읽는 표와 **"두 번째 문구가 보고되면 사용자를 RP로 돌려보내지 마세요"**를 적었습니다.

Upgrade notes

동작이 달라지는 곳은 로그인 화면 하나입니다: 인가 요청을 읽지 못한 이유가 404가 아닐 때 문구가 바뀌고 다시 시도 버튼과 Trace ID가 생깁니다. 정상 동작에서도, 소진된 request token에서도 아무것도 달라지지 않습니다 — 404의 문구는 전과 같습니다. API도, 상태 코드도, 저장되는 데이터도 그대로이고 마이그레이션도 설정 변경도 없습니다. 지원 창구에는 두 문구가 서로 다른 일을 뜻한다는 것만 알리시면 됩니다(docs/operations.md의 표) — 두 번째 문구는 이쪽 장애이므로 사용자를 RP로 돌려보내지 말고 그 자리에서 다시 시도하게 하세요. 되돌릴 때 이전 이미지도 그대로 동작합니다.


Docker image: resso:v0.9.76

Archive: resso-v0.9.76.tar.gz

SHA-256: 803f96b3d243530397c6eb0c295cb63f590ebe745e96928bf70a6675efbf0124

ReSSO v0.9.75

Choose a tag to compare

@hkjang hkjang released this 09 Sep 05:43

인증 없는 누구든 쿼리 파라미터 하나로 멀쩡한 데이터베이스에 대한 경보를 울릴 수 있었습니다. max_age호출자가 보내는 숫자인데, 인가 Endpoint는 그것을 그대로 최근 인증 시각 쿼리로 넘겼고 그 쿼리는 데이터베이스에게 그 숫자로 interval을 만들라고 합니다. interval이 담을 수 있는 한계(약 9.2e12초)를 넘는 값은 거기서 실패하며, 그 실패는 저장소가 멈춘 것과 구별되지 않습니다 — 요청은 302 error=server_error로 나가고 바로 앞 릴리즈가 더한 resso_authorization_errors_total{stage="auth_time"}을 올렸습니다. 운영자가 장애로 읽으라고 문서에 적혀 있는 바로 그 계열입니다. 그것을 받은 사람은 sso_sessions를 들여다보러 갔습니다.

수정

  • 이제 그런 값은 거절하지 않고 clamp합니다. 상한은 MaxInt32(2147483647초, 약 68년)입니다.
  • clamp는 아무것도 잃지 않습니다. SSO Session의 수명은 session_ttl_seconds가 상한이고 그 값은 30일로 제한되므로, 그보다 큰 max_age존재하는 어떤 Session이든 충족합니다. 답은 처음부터 정해져 있었고 계산만 되지 않았을 뿐입니다. 상한은 그 두 값이 같아지는 지점보다 60년 더 뒤입니다.
  • int64에도 담기지 않는 값은 거절하지 않고 같은 상한에 도달하게 했습니다. 그저 거대한 값은 clamp하면서 더 거대한 값만 거절하면 사양에 없는 선을 긋는 것이기 때문입니다. ParseInt가 범위 초과를 알려주며 포화시키므로 두 형태가 같은 자리로 모입니다.
  • 숫자가 아닌 값과 음수는 그대로 invalid_request입니다 — 그것들은 호출자에 관한 사실이고, 전과 달라지지 않습니다.
  • auth_time 단계 자체는 핸들러에 남습니다 — 두 조회 사이에 세션이 사라지는 경우와 저장소 자체의 실패가 여전히 그 자리를 씁니다. 다만 연동 테스트의 행은 잃었습니다: 그 쿼리가 읽는 테이블은 모두 앞의 세션 조회가 먼저 읽으므로 거대한 max_age가 그 단계에 닿는 유일한 길이었고, 이번 수정이 지운 것이 정확히 그 길입니다.
  • 대신 그 거대한 max_age테스트가 올바로 답하는 요청 쪽으로 옮겨, 코드가 나가는 것과 실패 계열을 건드리지 않는 것을 함께 고정했습니다. TestIntegrationMaxAgeForcesReauthenticationclamp되는 두 형태(interval 한계를 넘는 값, int64를 넘는 값)와 여전히 거절되는 음수를 지킵니다. 새 단언 셋이 수정 전 코드에서 모두 실제로 실패하는 것을 확인했습니다.
  • docs/compatibility.mdmax_age 행에 clamp를, docs/operations.mdresso_authorization_errors_total 항목에 **"이제 여섯 값 모두 이쪽 장애만 센다"**를 적었습니다.

Upgrade notes

동작이 달라집니다: max_age약 9.2e12초를 넘는 인가 요청이 302 error=server_error 대신 정상적으로 처리됩니다(그런 값은 어떤 Session이든 충족하므로 재인증 없이 코드가 나갑니다). 정상 동작에서는 아무것도 달라지지 않습니다 — 현실적인 max_age도, 숫자가 아닌 값과 음수의 invalid_request도 전과 같습니다. resso_authorization_errors_total{stage="auth_time"}이 v0.9.74에서 올라간 기록이 있다면 저장소 장애가 아니라 이 파라미터였을 수 있으므로 그 시각의 요청을 함께 보시면 됩니다. 마이그레이션도, 설정 변경도, 저장되는 데이터의 변화도 없고 되돌릴 때 이전 이미지도 그대로 동작합니다.


Docker image: resso:v0.9.75

Archive: resso-v0.9.75.tar.gz

SHA-256: a597755737f3da299e29324b8c4ee664a6b3a5929b1b13739a7ae44a13b20204

ReSSO v0.9.74

Choose a tag to compare

@hkjang hkjang released this 08 Sep 21:33

인가 Endpoint가 처리하지 못한 요청이 이 서비스의 어떤 신호에도 남지 않았습니다. 이 Endpoint가 만드는 자기 쪽 실패 여섯 중 넷은 RP의 redirect_uri로 302에 error=server_error를 실어 보냅니다 — 사양이 리다이렉트 가능한 오류를 거기 두라고 하므로 그 자체는 옳습니다. 문제는 302가 인가에 성공했을 때 나가는 상태와 같다는 것입니다. resso_http_requests_total은 어느 쪽이든 정상 Redirect로 세고 접근 로그도 status=302라, Realm의 모든 인가를 가져간 장애가 바쁘게 잘 도는 Endpoint와 구별되지 않았습니다 — 사람이 로그인 화면에 도달하지 못할 뿐이고, RP들은 여기서 아무도 볼 수 없는 오류를 받았습니다.

수정

  • 넷 중 셋은 store 에러를 그 자리에서 버려 로그 한 줄도 없었습니다SessionAuthenticatedRecently, CreateAuthorizationCode, CreateAuthorizationRequest. 즉 장애를 알아볼 방법이 이쪽에는 하나도 남아 있지 않았습니다.
  • 근거는 같은 모양의 Endpoint 둘에 이미 있었습니다. Token은 resso_token_errors_total, Introspection은 resso_introspection_errors_total정확히 이 이유로 존재합니다 — 응답의 겉모습이 정상과 같아서 요청 카운터가 둘을 구분하지 못하는 자리입니다. 인가에만 그 계열이 없었습니다.
  • 이제 resso_authorization_errors_total{stage}가 그것을 셉니다. stage는 여섯 값으로 고정 카디널리티입니다 — realm(경로의 Realm 조회), client(client_id 조회), sso_session(브라우저 세션 조회), auth_time(max_age 판정을 위한 최근 인증 시각), authorization_code(코드 생성), authorization_request(로그인 화면으로 넘길 요청 저장).
  • 리다이렉트되지 않고 여기서 500으로 답하는 앞의 둘도 같은 계열에 셉니다. 그 둘은 요청 카운터에도 이미 보이지만, 한 계열로 모아야 "처리하지 못한 인가"를 다른 계열과 조인하지 않고 한 번에 볼 수 있습니다. 그 둘은 각자의 로그 문구가 이미 문서에 적혀 있으므로 로그는 그대로 두고 카운터만 더했습니다.
  • 호출자 때문에 생기는 답은 세지 않습니다. 없는 Realm의 404도, 등록되지 않은 client_id의 400도 이 계열을 만들지 않습니다 — 이 계열은 이쪽이 답을 내지 못한 것만 담습니다.
  • 새 연동 테스트가 테이블을 차례로 RENAME으로 숨기며(realms·clients·sso_sessions·authorization_codes·authorization_requests) 여섯 단계를 모두 확인하고, 수정 전 코드에서 여섯 건이 모두 실제로 실패하는 것을 확인했습니다(어느 stage도 계열이 없었습니다). auth_time만은 숨길 테이블이 없어서(그 쿼리가 읽는 테이블은 모두 앞의 세션 조회가 먼저 읽습니다) **데이터베이스가 interval로 만들 수 없는 max_age**로 그 쿼리 안에서만 실패하게 했습니다. 정상 인가 둘(로그인 화면으로 park, 세션 재사용으로 코드 발급)과 호출자 쪽 답 둘이 계열을 만들지 않는 것, 테이블 복구 후 회복도 같은 테스트가 함께 지킵니다.
  • README 지표 표에 한 줄, docs/operations.md 경보 절에 stage 값 목록과 "302라 성공한 인가와 상태가 같다"는 읽는 법을 적었습니다.

Upgrade notes

동작은 달라지지 않습니다: 응답도, 상태 코드도, 저장되는 데이터도 전과 같고 마이그레이션도 없습니다. /metrics에 계열 하나(resso_authorization_errors_total, 라벨 stage 여섯 값)가 늘어나고, 앞서 조용히 버려지던 store 실패 셋에 서버 로그 an authorization request could not be served가 생깁니다. 스크랩 직후 이 계열이 0이 아닌 값으로 보인다면 새 결함이 아니라 지금까지 302에 가려져 있던 장애이므로, stage가 가리키는 단계부터 보시면 됩니다. 경보를 새로 거신다면 docs/operations.md의 항목을 그대로 쓰시면 되고, 되돌릴 때 이전 이미지도 그대로 동작합니다.


Docker image: resso:v0.9.74

Archive: resso-v0.9.74.tar.gz

SHA-256: 4d400bb5326450f8668a4d751fa85759726757d18006749592104775c9990f97

ReSSO v0.9.73

Choose a tag to compare

@hkjang hkjang released this 08 Sep 19:13

id_token_hint가 로그인 화면 앞에서 멈춰 있었습니다. RP는 이 파라미터로 **"이 계정을 갱신하는 중"**이라고 지목합니다. 그런데 인가 Endpoint는 그것을 브라우저에 이미 있는 Session과만 대조했습니다. 그 대조에 실패한 요청은 로그인 화면으로 보내지는데, 보관되는 authorization_requests 행에는 hint의 흔적이 없어서, 그 화면에서 로그인한 사람이 누구든 코드가 나갔습니다.검사가 가장 필요한 경로에만 검사가 없었습니다 — 요청이 그 화면까지 오는 이유가 대개 거기 있던 Session이 지목된 계정이 아니었기 때문입니다.

수정

  • RP는 한 계정을 물었는데 다른 사람을 건네받았고, 응답에는 그 사실을 알리는 것이 아무것도 없었습니다. 코드는 평범한 성공으로 돌아갔습니다. OIDC Core 3.1.2.1은 hint가 지목한 사람이 로그인하지 않았으면 코드가 아니라 오류를 답하라고 합니다.
  • 약속은 이미 문서에 적혀 있었습니다. docs/compatibility.md는 "지정한 계정과 다르면 재인증을 요구한다"고 적고 있었지만, 그 재인증이 실제로 일어난 뒤에는 아무도 결과를 보지 않았습니다.
  • 이제 hint가 요청과 함께 보관됩니다. 마이그레이션 015가 authorization_requests.id_token_hint_subject를 더하고, login인가 코드를 만들기 직전에 방금 인증된 사용자와 대조합니다. 다르면 **403 account_mismatch**이고 아무것도 발급하지 않습니다.
  • 인가 요청은 일부러 소진하지 않습니다. 같은 화면에서 지정된 계정으로 다시 로그인하면 흐름이 이어집니다 — RP로 돌아가 새 request token으로 처음부터 시작할 필요가 없습니다.
  • 응답은 어느 계정인지 밝히지 않습니다. 키보드 앞의 사람은 방금 자신이 그 사람이 아님을 증명했습니다.
  • 로그인 자체는 성공했으므로 세션과 쿠키는 그대로 둡니다. resso_login_attempts_total{result="success"}도 그대로 오릅니다 — 자격 증명은 받아들여졌고 세션은 진짜입니다. 코드를 발급하지 않은 사실은 사유가 맞는 자리인 트레일에 LOGIN_SUCCESS result=PARTIAL + reason=id_token_hint_mismatch로 남습니다.
  • max_age는 함께 보관하지 않았습니다. 로그인 화면을 거친 요청은 항상 새 Session을 만들어 auth_time이 그 시점이므로 정의상 충족됩니다. 다음 사람이 다시 따져보지 않도록 그 판단을 docs/compatibility.md에 적었습니다.
  • 새 연동 테스트가 세션 없는 브라우저로 hint 붙은 인가 요청을 보내 로그인 화면으로 park시킨 뒤 다른 계정으로 로그인하고, 수정 전 코드에서 네 건이 모두 실제로 실패하는 것을 확인했습니다(200 + code=가 실린 redirect_to, error != account_mismatch, PARTIAL 감사 0건). 지정된 계정으로 같은 request token을 써서 다시 로그인하면 등록된 redirect_uri로 코드가 나가는 것과, 거절 문구가 hint 계정 이름을 노출하지 않는 것도 같은 테스트가 함께 지킵니다.
  • docs/operations.mdresult=PARTIAL 항목에는 이 기록 하나만은 장애가 아니라 정책이라는 예외를 적었습니다 — 그곳의 다른 모든 PARTIAL은 무언가 실패한 것입니다.

Upgrade notes

동작이 달라집니다: id_token_hint가 붙은 인가 요청이 로그인 화면을 거칠 때, **hint가 지목한 계정이 아닌 사람이 로그인하면 인가 코드 대신 403 account_mismatch**가 나갑니다. 정상 동작에서는 아무것도 달라지지 않습니다 — hint를 보내지 않는 RP와, 지목한 계정으로 로그인하는 경우는 전과 같습니다. 이 응답이 보인다면 RP가 오래된 hint를 계속 보내고 있는지 먼저 확인하시고, 사용자에게는 같은 화면에서 지정된 계정으로 다시 로그인하면 그대로 이어진다고 안내하시면 됩니다. LOGIN_SUCCESS result=PARTIAL 기록이 전에 없던 자리에서 생길 수 있는데, 상세가 reason=id_token_hint_mismatch인 것만은 장애가 아니라 정책입니다(docs/operations.md). 마이그레이션 015가 authorization_requests에 컬럼 하나를 더하며 기본값이 빈 문자열이라 처리 중인 요청에는 영향이 없고, 되돌릴 때 이전 이미지도 그대로 동작합니다.


Docker image: resso:v0.9.73

Archive: resso-v0.9.73.tar.gz

SHA-256: a27ba680b9f90594461c3e52d659e9874f7aeba8edcc0b4791c5d877cc79f654