generated from ttipo/project-base
docs: 12단계 계정 보안 — 계약 5절 · 디자인 보드 넷 · README #9
@@ -683,8 +683,10 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- 코드 틀림 — 칸 아래 빨간 글(`AUTH_006` 사전 문구 "코드가 올바르지 않습니다. 휴대폰의 날짜와 시간이 「자동」으로 맞춰져 있는지 확인해 주세요."). 칸을 비우고 초점은 그 칸, 알림은 없다.
|
||||
- 시도 과다 — 위쪽 빨간 상자(로그인 오류와 같은 자리 · 같은 모양)에 `REQ_001` 사전 문구. 코드 실패는 비밀번호 틀림과 따로 센다(같은 주소 15분 5회 · 하루 20회). 칸 · 단추는 그대로 둔다.
|
||||
- 확인 중 — 「확인하는 중…」(로그인 단추와 같은 모양 · `aria-busy`), 칸은 읽기만. 맞으면 받은편지함으로 — 로그인 전에 가려던 주소가 있으면 그곳으로.
|
||||
- 이 화면에 10분 넘게 머물면 [확인] 때 「다시 로그인해 주세요」와 함께 처음 화면으로 간다. 새로고침도 처음 화면이다(비밀번호는 화면 메모리에만 있다).
|
||||
- 이 화면에 10분 넘게 머물면 비밀번호를 바로 지우고, [확인] 때 처음 화면으로 가 위쪽 상자에 "코드를 넣는 시간이 지났습니다. 다시 로그인해 주세요."를 보인다. 새로고침도 처음 화면이다(비밀번호는 화면 메모리에만 있다).
|
||||
- 코드 단계에서 비밀번호가 틀리면(그사이 바뀜) 처음 화면의 위쪽 오류 상자에 보인다 — 지금 로그인 화면과 같은 자리. 6자리가 아니면 보내지 않고 칸 아래에 `AUTH_007` 사전 문구.
|
||||
- 로그인 기록에는 코드까지 맞으면 「성공 · 2단계 인증」, 틀리면 「실패 — 코드 틀림」으로 남는다.
|
||||
- 성공에 쓴 코드는 2분 동안 다시 받지 않는다 — 30초 안에 두 곳(PC · 휴대폰)에서 연달아 로그인하면 두 번째는 「코드가 올바르지 않습니다…」이고, 앱에 다음 코드가 뜰 때 넣는다.
|
||||
|
||||
### 설정 › 계정 › 2단계 인증 (`SettingsTwoFactor`)
|
||||
|
||||
@@ -697,7 +699,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- 「켜는 차례」 1 · 2 · 3(청록 굵은 숫자 — 동그라미 없음): 인증 앱 설치(Google · Microsoft Authenticator 처럼 6자리 코드를 만드는 앱) → [2단계 인증 켜기]를 눌러 QR 찍기 → 코드와 지금 비밀번호를 넣으면 켜진다.
|
||||
- **켜져 있을 때** 상자 아래: "휴대폰을 바꿀 때는 2단계 인증을 끈 뒤 새 휴대폰으로 다시 켜 주세요 — 끌 때 지금 휴대폰의 코드가 필요합니다." · "휴대폰을 잃어버렸거나 인증 앱을 지웠으면 관리자에게 문의해 주세요." · "메일 프로그램은 앱 비밀번호로 연결합니다."
|
||||
- **켜기 창**(모달 620 — `wide`, 필터 고치기 창과 같은 폭 · 처음 초점은 코드 칸)
|
||||
- ① "인증 앱으로 QR 코드를 찍어 주세요" / "인증 앱에서 [+] → 「QR 코드 스캔」을 고르고 이 그림을 찍습니다." + QR 그림
|
||||
- ① "인증 앱으로 QR 코드를 찍어 주세요" / "인증 앱에서 [+] → 「QR 코드 스캔」을 고르고 이 그림을 찍습니다." + QR 그림(160 — 실제 otpauth 주소는 오류 정정 M 에서 49칸이라 보드의 128 보다 키웠다)
|
||||
- "찍을 수 없으면 이 키를 입력하세요" + 32자 키(4자씩 띄움, 고정폭) + [복사](36 — 「키를 복사했습니다.」) + "앱에는 「ARA SYSTEM (ara.kim@arasystem.it.kr)」로 저장됩니다."
|
||||
- ② "앱에 표시된 6자리 코드" ③ "지금 비밀번호" → [켜기] [취소]
|
||||
- 칸 아래 오류(알림 없이, 첫 잘못된 칸에 초점): 코드 `AUTH_006` · 지금 비밀번호 `AUTH_005`("지금 비밀번호가 올바르지 않습니다."). 둘 다 틀렸으면 코드부터 — 서버가 코드를 먼저 본다.
|
||||
@@ -728,7 +730,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **만들기 창**(400 · 처음 초점 이름 칸): 「이름」 + "어느 프로그램 · 기기에서 쓰는지 알아볼 이름입니다. 40자까지." → [만들기] [취소].
|
||||
칸 아래: "이름을 넣어 주세요." · "같은 이름의 앱 비밀번호가 있습니다."(앞뒤 공백을 지운 값으로 화면이 먼저 본다) · 그사이 다른 창에서 5개를 채웠으면 `AUTH_009` 사전 문구.
|
||||
- **만든 결과 창**(620 · 처음 초점 [복사]): "앱 비밀번호를 만들었습니다" / "‘회사 PC 아웃룩’ — 메일 프로그램의 비밀번호 칸에 아래 값을 넣어 주세요."
|
||||
- 값은 고정폭으로 4자씩 떨어뜨려 보이지만 띄어쓰기는 없다 — [복사](36)는 40자 그대로. "40자입니다. 보기 좋게 4자씩 띄웠지만 띄어쓰기는 없습니다."
|
||||
- 값은 고정폭으로 4자씩 떨어뜨려 보이지만 띄어쓰기는 없다 — [복사](44 — 창의 주 동작이라 보드대로)는 40자 그대로. "40자입니다. 보기 좋게 4자씩 띄웠지만 띄어쓰기는 없습니다."
|
||||
- 경고 "이 창을 닫으면 다시 볼 수 없습니다. 잊어버리면 지우고 새로 만들어 주세요." + 설정값 요약 + [닫기].
|
||||
- 복사 「앱 비밀번호를 복사했습니다.」 / 못 하면 「앱 비밀번호를 복사하지 못했습니다. 값을 직접 골라 복사해 주세요.」
|
||||
- [닫기] · Esc 로 닫으면 목록 맨 위에 새 줄이 생기고 값은 화면 메모리에서도 지운다. 복사하지 않고 닫아도 다시 묻지 않는다.
|
||||
@@ -746,7 +748,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
"이 브라우저를 뺀 2곳(Edge · Windows, Safari · iPhone)의 웹메일 로그인을 끊습니다. 끊긴 곳은 다음 동작에서 바로 로그인 화면으로 돌아갑니다." "메일 프로그램은 끊기지 않습니다 — 앱 비밀번호를 지워 끊어 주세요."
|
||||
[모두 로그아웃](청록 주 단추 — 데이터를 지우는 일이 아니다) [취소] → 「다른 곳의 로그인 2곳을 끊었습니다.」
|
||||
- 끊긴 브라우저는 다음 요청에서 바로 로그인 화면으로 간다(열린 실시간 알림도 30초 안에) — 계약 5절 "로그인된 브라우저".
|
||||
- **최근 로그인**: 「최근 로그인」 + "최근 90일 · 웹메일과 메일 프로그램" · 오른쪽 전환 [전체 · 실패만](`.segmented`). 표: 시각 · 방식 · IP · 기기 · 결과.
|
||||
- **최근 로그인**: 「최근 로그인」 + "최근 90일 · 웹메일과 메일 프로그램" · 오른쪽 전환 [전체 · 실패만](`.segmented--outline` — role=group + `aria-pressed`). 표: 시각 · 방식 · IP · 기기 · 결과.
|
||||
- 방식: "웹메일" / "메일 프로그램 · IMAP" · "메일 프로그램 · SMTP" + 앱 비밀번호로 들어왔으면 아래 줄 "앱 비밀번호 ‘회사 PC 아웃룩’"(지운 앱 비밀번호도 그 이름 그대로).
|
||||
- 기기: 웹메일은 "Chrome · Windows", 메일 프로그램은 "—". 나라 · 지역은 보이지 않는다.
|
||||
- 결과: 「성공」 · 「성공 · 2단계 인증」 · 「실패 — 비밀번호 틀림」 · 「실패 — 코드 틀림」 · 「실패 — 앱 비밀번호 필요」(2단계를 켠 계정에 메일 프로그램이 웹메일 비밀번호로 연결하려 함). 실패는 빨간 굵은 글자만 — 뱃지 없음.
|
||||
|
||||
+29
-12
@@ -14,6 +14,8 @@
|
||||
모바일 앱(Flutter)의 인증 방식(토큰 발급·`flutter_secure_storage`)은 앱 착수 때 이 문서에 추가한다.
|
||||
- **배포 형태: 화면과 API 는 같은 주소(origin)를 쓴다.** 운영은 프론트 nginx 가 `/api` 를 백엔드로 넘기고,
|
||||
개발은 vite 프록시가 넘긴다. 그래서 CORS 가 필요 없고 CSP `connect-src 'self'` 로 충분하다.
|
||||
- **요청 대상에 날 `\` · `#` · 제어 문자 · 공백 · 0x80 이상 바이트가 있으면 라우팅 전에 400 `VAL_001` 이다**(12단계 — 백엔드 `request-target` 미들웨어, 앞단 nginx 도 막는다).
|
||||
Express 가 이런 대상을 다르게 읽어 앞단이 닫은 경로가 백엔드에서 열렸다(2026-10-31 리뷰). 제어 문자 · 날 UTF-8 은 Node 의 HTTP 파서가 앱 전에 거절한다(JSON 이 아닌 400). 브라우저는 이런 요청을 보내지 않는다.
|
||||
- **경로는 대소문자를 가린다** — 이 문서의 경로는 모두 소문자이고, 대소문자 변형(`/API/…` · `/api/Messages/…` · `…/LARGE-FILES/…` · `/api/public-links/…/FILES/…`)은
|
||||
처리기에 닿지 않고 404 `RES_001` 이다(2026-10-30 — 앞단 nginx 의 받기 location 이 놓친 변형이 동시 연결 상한을 비켜 갔다, 4절 "운영 확인").
|
||||
- **출처 검사(CSRF)**: 상태를 바꾸는 요청(POST·PATCH·PUT·DELETE)은 브라우저가 붙이는 `Sec-Fetch-Site`·`Origin` 을 검사한다.
|
||||
@@ -829,7 +831,7 @@ data: {"reason":"token-expiring"}
|
||||
그래서 서버와 엔진 사이가 10분 넘게 끊겼다 다시 이으면, 그동안 온 메일은 `state` 로만 알린다(목록에는 나타나고 바탕화면 알림은 없다).
|
||||
"보낸 사람이 나"는 토큰의 주소와 발신 ID(별칭) 주소로 본다(대소문자 무시).
|
||||
- 오래 열린 엔진 연결은 엔진의 동시 요청 한도(4)를 잡아먹지 않는다 — 한 직원의 스트림 8개를 연 채 그 직원 · 다른 직원의 보통 요청과 동시 요청 6개가 모두 됐다(2026-10-28 확인).
|
||||
- 연결은 액세스 토큰이 끝날 때 닫는다(최대 1시간 — 그 전에 `end`). 로그아웃 · 비밀번호 변경으로 세션을 끊어도 다른 API 처럼 액세스 토큰이 끝날 때까지는 이어진다(1절 세션 규칙).
|
||||
- 연결은 액세스 토큰이 끝날 때 닫는다(최대 1시간 — 그 전에 `end`). 12단계부터 로그아웃 · 비밀번호 변경 · 세션 끊기로 끊은 세션의 연결은 다음 ping 때 `end` 없이 닫는다(계정을 더 쓸 수 없을 때와 같은 길 — 5절 "로그인된 브라우저").
|
||||
서버가 종료될 때는 열린 연결을 `end` 로 먼저 닫는다 — 오래 열린 연결이 종료를 붙잡지 않게.
|
||||
알려진 한계: 잠자기 · VPN 재연결 · 망 바꿈으로 반쯤 열린 연결은 서버가 ping 을 쓰다 실패할 때(OS 기본 15분 안팎)까지 자리를 잡는다 —
|
||||
그사이 그 직원의 연결이 5개에 닿으면 새 연결은 429 이고 화면은 1분 확인으로 이어진다.
|
||||
@@ -2080,7 +2082,7 @@ interface FileUploadCreate {
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| 코드 | 숫자 6자리 · 30초 · SHA-1(인증 앱 기본값). 엔진은 앞뒤 한 단계(±30초)까지 받는다 |
|
||||
| 같은 코드 다시 쓰기 | 같은 직원이 성공에 쓴 코드는 2분 동안 다시 받지 않는다(코드가 받아지는 90초보다 길게 — 로그인 · 설정 모두) → `AUTH_006` |
|
||||
| 같은 코드 다시 쓰기 | 같은 직원이 성공에 쓴 코드는 2분 동안 다시 받지 않는다(코드가 받아지는 90초보다 길게 — 로그인 · 설정 모두) → `AUTH_006`. 같은 코드가 다른 요청에서 확인되는 중이어도 `AUTH_006`(코드 실패로 센다) |
|
||||
| 코드 실패 | 같은 주소 15분 5회 · 24시간 20회. 같은 IP 는 로그인 실패 한도(1분 20회)에 함께 센다 → 429 `REQ_001` |
|
||||
| 등록 시간 | 등록을 시작하고 10분 안에 켠다. 직원마다 기다리는 등록은 하나다(새로 시작하면 앞의 것은 버린다) |
|
||||
| 앱 비밀번호 | 엔진 상한(기본 5개 — `x:Authentication.maxAppPasswords`). 이름 1~40자(앞뒤 공백을 지운 값, JS 문자열 길이). 권한은 메일 프로그램만 |
|
||||
@@ -2113,7 +2115,9 @@ interface FileUploadCreate {
|
||||
1. 앱 비밀번호 모양이면 거절한다.
|
||||
2. 1절처럼 실패 한 번을 차지하고 엔진에 본인 Basic 으로 묻는다(지금의 `verifyCredentials`).
|
||||
3. 200 → 2단계 인증이 꺼진 직원이다. 로그인한다(코드가 와도 보지 않는다).
|
||||
4. **402**(비밀번호는 맞고 엔진이 2단계를 요구) → 차지한 비밀번호 실패를 돌려준다. `otpCode` 가 없으면 `twoFactorRequired` 로 답한다.
|
||||
4. **402**(비밀번호는 맞고 엔진이 2단계를 요구) → 대리 접근 프로필로 정식 주소를 얻는다(엔진은 별칭 권한보다 MFA 를 먼저 판정한다).
|
||||
막는 계정(관리자 등)이면 `AUTH_004` 로 답하고 비밀번호 실패로 센다 — 맞는 비밀번호만 세지 않으면 그 차이로 비밀번호가 드러난다. 한도 · 기억 · 로그인 기록은 정식 주소로 잡는다.
|
||||
그다음 차지한 비밀번호 실패를 돌려준다. `otpCode` 가 없으면 `twoFactorRequired` 로 답한다.
|
||||
있으면: 다시 쓴 코드인지 보고 → 코드 실패 한 번을 차지하고 → 엔진 `POST /api/auth`(`accountSecret` + `mfaToken`)에 묻는다.
|
||||
`authenticated` 면 로그인한다(차지한 것을 돌려주고 코드를 2분 기억). `failure` 면 `AUTH_006`.
|
||||
5. 401 → `AUTH_004`.
|
||||
@@ -2126,7 +2130,9 @@ interface FileUploadCreate {
|
||||
- 코드 단계는 비밀번호를 화면 메모리에만 둔다. 성공 · [다른 계정으로 로그인] · 화면을 떠날 때 지운다.
|
||||
코드 단계에 10분 넘게 머물면 비밀번호를 지우고 첫 단계로 돌아간다(새로고침도 첫 단계).
|
||||
- 코드 칸은 `inputmode="numeric"` · `autocomplete="one-time-code"` 이다. 붙여 넣은 글에서 숫자만 남긴다(공백 · `-`).
|
||||
- `AUTH_006` 은 칸 아래에 보이고(알림 없음) 칸을 비운다. `AUTH_004` 는 첫 단계로 돌아가 비밀번호 칸 아래에 보인다.
|
||||
- `AUTH_006` 은 칸 아래에 보이고(알림 없음) 칸을 비운다. 6자리가 아니면 보내지 않고 칸 아래에 `AUTH_007` 사전 문구를 보인다.
|
||||
- `AUTH_004` 는 첫 단계로 돌아가 위쪽 오류 상자에 보인다(지금 로그인 화면과 같은 자리 — 주소는 남기고 비밀번호는 비우며 칸에 `aria-invalid`).
|
||||
- 10분이 되면 비밀번호를 바로 지운다. [확인]을 누르면 첫 단계로 돌아가 위쪽 상자에 "코드를 넣는 시간이 지났습니다. 다시 로그인해 주세요."를 보인다.
|
||||
|
||||
### 비밀번호 바꾸기 (`PUT /api/auth/password`) — 4단계에 더하는 것
|
||||
|
||||
@@ -2137,7 +2143,8 @@ interface FileUploadCreate {
|
||||
- 비밀번호를 먼저 본인 Basic 으로 확인한다 — 엔진은 지금 비밀번호 틀림과 코드 틀림을 같은 `forbidden` 으로 거절하기 때문이다.
|
||||
- 200(2단계 꺼짐) → 지금처럼 본인 자격으로 바꾼다.
|
||||
- 402(2단계 켜짐, 비밀번호 맞음) → 게이트웨이 대리 접근으로 `x:AccountPassword/set`(`currentSecret` + `secret` + `otpAuth.otpCode`)을 보낸다. 2단계 인증은 그대로 남는다.
|
||||
- `otpCode` 가 없으면 400 `AUTH_007`(다른 탭에서 켠 경우 — 화면이 코드 칸을 보인다). 틀리거나 다시 쓴 코드면 400 `AUTH_006`(코드 실패로 센다).
|
||||
- `otpCode` 가 없으면 400 `AUTH_007`(다른 탭에서 켠 경우 — 화면이 코드 칸을 보인다). 틀리거나 다시 쓴 코드면 400 `AUTH_006`(코드 실패로 센다). `otpCode: null` 은 400 `VAL_001`(로그인도 같다).
|
||||
- 새 비밀번호가 엔진 규칙에 어긋나 거절되면 그 코드는 쓰지 않은 것으로 둔다. 바뀌었거나 결과를 모르면 쓴 것으로 본다(2분 다시 받지 않는다).
|
||||
- 앱 비밀번호 모양의 지금 비밀번호는 엔진에 묻지 않고 `AUTH_005` 다(세지 않는다 — 4단계 규칙과 같다).
|
||||
- **바뀌면 그 직원의 앱 비밀번호를 모두 지운다**(2단계 인증과 상관없이 — 대리 접근 `x:AppPassword/set` 의 destroy, Google 과 같다).
|
||||
- 비밀번호가 샌 동안 누군가 웹메일에서 만든 앱 비밀번호가 남지 않게 한다.
|
||||
@@ -2169,9 +2176,9 @@ interface FileUploadCreate {
|
||||
- QR 은 화면이 `otpauthUrl` 로 그린다. 새 프론트 의존성이 든다 — 작은 MIT QR 부호기 하나, `v-html` 없이 SVG 로 그린다(고르는 것은 구현 때).
|
||||
- 켜기
|
||||
1. 받은 코드를 등록의 비밀로 먼저 확인한다(±1 단계). 엔진은 등록 때 코드를 보지 않는다 — 잘못 등록하면 직원이 끄지도 못한다.
|
||||
2. 맞으면 대리 접근으로 `currentSecret` + `otpAuth.otpUrl` 을 보낸다.
|
||||
3. 지금 비밀번호가 틀리면 `AUTH_005` 다(로그인 실패 한도로 센다 — 비밀번호 바꾸기와 같다). 켠 코드는 2분 기억한다.
|
||||
- 끄기: 비밀번호를 본인 Basic 으로 먼저 확인(402) → 다시 쓴 코드인지 확인 → 대리 접근으로 `currentSecret` + `otpAuth: { otpCode, otpUrl: null }`. 이미 꺼져 있으면 200 이다(같은 결과).
|
||||
2. 맞으면 지금 비밀번호를 본인 Basic 으로 본다 — 틀리면 `AUTH_005`(로그인 실패 한도로 센다 — 비밀번호 바꾸기와 같다. 그 코드는 쓰지 않은 것으로 남아 비밀번호만 고쳐 같은 코드로 다시 보낼 수 있다), 402 면 그사이 다른 곳에서 켜진 것이라 `AUTH_008`.
|
||||
3. 대리 접근으로 `currentSecret` + `otpAuth.otpUrl` 을 보낸다. 켠 코드는 2분 기억하고 등록의 비밀은 바로 지운다(`enabledAt` 기록이 실패해도 성공 — 엔진이 기준).
|
||||
- 끄기: 비밀번호를 본인 Basic 으로 먼저 확인(402) → 다시 쓴 코드인지 확인 → 대리 접근으로 `currentSecret` + `otpAuth: { otpCode, otpUrl: null }`. 이미 꺼져 있으면 200 이다(같은 결과). 엔진에서 꺼진 뒤 `enabledAt` 기록 지우기가 실패해도 200 이다(error 로그 — 엔진이 기준).
|
||||
- 켜고 끄는 일은 웹메일 세션을 끊지 않는다 — 로그인 기록 화면에 [다른 곳 모두 로그아웃]이 따로 있다. 이미 열린 IMAP 연결은 다시 연결할 때까지 남는다(알려진 한계).
|
||||
- 같은 직원의 2단계 인증 바꾸기(등록 · 켜기 · 끄기 · 2단계를 켠 직원의 비밀번호 바꾸기)는 한 번에 하나다 — 처리 중이면 429 `REQ_001`.
|
||||
- 휴대폰 바꾸기: 끈 뒤 다시 켠다(켜진 채 다시 등록하기는 처음 판에 없다).
|
||||
@@ -2199,7 +2206,7 @@ interface FileUploadCreate {
|
||||
{ "name": "회사 PC 아웃룩" }
|
||||
→ { "id": "c", "name": "회사 PC 아웃룩", "createdAt": "…", "lastUsed": null, "password": "app_…(40자)" }
|
||||
|
||||
// DELETE /api/app-passwords/{id} → 200 (data: null) — 이미 없으면 200
|
||||
// DELETE /api/app-passwords/{id} → 200 (data: null) — 이미 없거나 모양이 틀린 id 도 200(아무것도 하지 않는다)
|
||||
```
|
||||
|
||||
- 만들기는 대리 접근 `x:AppPassword/set` 의 create 다:
|
||||
@@ -2209,6 +2216,7 @@ interface FileUploadCreate {
|
||||
- 상한을 넘으면 엔진이 `overQuota` 로 거절한다 → 409 `AUTH_009`. 이름이 비었거나 40자를 넘거나 이미 있는 이름(앞뒤 공백을 지운 값이 같음)이면 400 `VAL_001` — 화면이 먼저 보고 칸 아래에 알린다.
|
||||
- 목록은 최근에 만든 것이 위다.
|
||||
화면의 "n / 5개"는 엔진 기본값 5 를 상수로 쓴다 — 엔진 상한을 바꾸면 화면 상수도 함께 바꾼다.
|
||||
같은 직원의 만들기는 한 번에 하나다 — 처리 중이면 429 `REQ_001`(같은 이름 두 요청이 함께 만들어지지 않게).
|
||||
- 2단계 인증을 켜지 않은 직원도 만든다(기기마다 따로 끊기 편하다).
|
||||
- 쓰는 곳: IMAP 993 · SMTP 465(사용자 이름 = 메일 주소).
|
||||
- 쓰이지 않는 곳: 웹메일 로그인 · 비밀번호 바꾸기 · 2단계 끄기.
|
||||
@@ -2268,6 +2276,7 @@ interface FileUploadCreate {
|
||||
|
||||
- 남기는 것
|
||||
- 웹메일: 서버가 판정한 로그인 시도(성공 · `AUTH_004` · `AUTH_006`). 한도로 막은 요청(429) · 사내 도메인이 아닌 주소 · 막은 계정은 남기지 않는다.
|
||||
사내 목록에 없는 주소의 `AUTH_004` 도 남기지 않는다(아무 주소나 뿌려 표를 채우지 못하게 — 웹훅과 같은 규칙, 사내 목록을 읽지 못하면 남긴다). 2단계 로그인의 한도 · 기록은 엔진이 알려 준 정식 주소로 잡는다(별칭으로 들어와도).
|
||||
- 메일 프로그램: 엔진 웹훅(아래)의 IMAP · SMTP 이벤트.
|
||||
- 남기지 않는 것: 갱신(`/auth/refresh`), 게이트웨이 대리 접근, 엔진 HTTP 리스너의 이벤트(웹메일이 백엔드 IP 로 남기는 것 — 서버 기록과 겹친다).
|
||||
- 90일 보존, 커서 페이지(기본 20 · 최대 100). `isSuccess=false` 면 실패만 준다.
|
||||
@@ -2286,15 +2295,20 @@ interface FileUploadCreate {
|
||||
- `data.remoteIp` · `data.accountName`(주소, 앱 비밀번호면 로컬 이름) · `data.details`
|
||||
- `data.id`: 앱 비밀번호 번호. `details` 가 앱 비밀번호 표시일 때만 쓴다 — IMAP 실패의 `id` 는 명령 태그다.
|
||||
- 사내 도메인의 직원만 남긴다. 사내 목록에 없는 주소의 실패와 막은 계정은 버린다(엔진은 서버 전체의 이벤트를 보낸다).
|
||||
- 엔진 이벤트 `id` 로 겹침을 거른다(엔진이 다시 보낸 묶음). 곧바로 200 으로 답한다(엔진이 5초 기다린다).
|
||||
- 겹침은 엔진 이벤트 `id` 가 아니라 **내용 지문**(type + createdAt + data)의 유일 인덱스로 거른다 — 엔진은 다시 보낼 때마다 id 를 새로 붙이고, 실패한 묶음에 새 이벤트를 합쳐 보낸다.
|
||||
createdAt 이 초 단위라 같은 초 · 같은 연결의 똑같은 이벤트(명령 태그가 없는 SMTP 실패 둘 등)는 하나로 합쳐진다(알려진 한계).
|
||||
- DB 에 넣은 뒤 200 으로 답한다(1,000행 안팎으로 나눠 넣는다 — 한 문장의 매개변수 상한). DB 가 실패하면 5xx 로 답해 엔진이 다시 보내게 한다(지문 덕에 겹치지 않는다). 빈 묶음 `{"events":[]}` 도 200 이다 — setup 이 받는 곳을 이것으로 확인한다.
|
||||
- setup 이 엔진 웹훅에 붙이는 머리글 `X-Arasystem-Setup`(setup 표시 · 소금 · 키 지문)은 쓰지 않고 남기지 않는다. 거절 warn 은 같은 까닭이면 1분에 한 번만(그사이 거절한 수와 함께) 남긴다.
|
||||
- 이 경로만 다르게 둔다
|
||||
- 요청 수 한도가 없다(`@SkipThrottle`). 출처 검사는 `Origin` 이 없어 통과한다.
|
||||
- JSON 한도를 따로 둔다(본문 4MB · 배열 하나 10,000 · 값 200,000) — 공격이 오면 1초 묶음이 커진다.
|
||||
- 엔진의 대기열은 메모리에 있다. 서버가 5분 넘게 받지 못하거나 엔진이 다시 뜨면 그동안의 메일 프로그램 기록은 빠진다(알려진 한계).
|
||||
한도(10,000개 · 4MB)를 넘는 묶음도 거절하므로, 실패 이벤트가 그만큼 쏟아지는 공격 동안에는 엔진이 묶음을 버릴 때까지(5분) 기록이 빠질 수 있다.
|
||||
|
||||
### 엔진 설정 (setup — release-ops)
|
||||
|
||||
- `x:WebHook` 하나를 둔다(setup 표시로 찾는다).
|
||||
- `x:WebHook` 하나를 둔다. 0.16.27 웹훅에는 이름 칸이 없어 머리글 `X-Arasystem-Setup: [setup.mjs] key=<소금>.<지문>` 으로 찾는다(엔진이 서명 키를 `****` 로만 돌려주므로 키가 바뀌었는지도 이 지문으로 안다 — 소금은 쓸 때마다 새로).
|
||||
엔진 DNS 가 Custom(운영 — 리졸버)이어도 `backend` 를 푼다 — 메일 컨테이너의 resolv.conf 가 도커 DNS 다(2026-10-31 확인).
|
||||
- url `http://backend:<BACKEND_PORT>/api/internal/mail-engine-events`, `signatureKey` = `MAIL_WEBHOOK_SECRET`
|
||||
- `eventsPolicy: include` + `auth.success` · `auth.failed` · `auth.mfa-required` — 기본(`exclude` + 빈 목록)은 모든 이벤트를 보낸다.
|
||||
- `level: trace` — 이 판은 웹훅에 수준을 쓰지 않지만 새 판 문서는 쓴다(기본 info 면 실패 이벤트가 빠진다).
|
||||
@@ -2342,7 +2356,10 @@ interface FileUploadCreate {
|
||||
|
||||
- IMAP · SMTP 에서 2단계를 켠 계정에 맞는 비밀번호를 넣으면 "2단계 필요"가 나와 비밀번호가 맞았는지 알 수 있다. 엔진 자동 차단은 틀린 시도만 센다(들어오지는 못한다).
|
||||
- 2단계를 켜거나 앱 비밀번호를 지워도 이미 열린 IMAP 연결은 엔진이 끊지 않는다(지운 앱 비밀번호의 연결은 다음 명령에서 멈춘다).
|
||||
- 웹훅이 잃는 기록이 있다(엔진 재시작 · 서버가 5분 넘게 받지 못함).
|
||||
- 웹훅이 잃는 기록이 있다(엔진 재시작 · 서버가 5분 넘게 받지 못함 · 한도를 넘는 묶음). 같은 초 · 같은 연결의 똑같은 이벤트는 하나로 합쳐진다.
|
||||
- 성공에 쓴 코드는 2분 동안 다시 받지 않으므로, 30초 안에 두 곳에서 연달아 로그인하면 두 번째는 `AUTH_006` 이고 다음 코드를 기다려야 한다.
|
||||
- 로그인 기록의 `appPassword.name` 은 웹메일 밖(엔진 관리 화면)에서 만든 앱 비밀번호면 `null` 이다. `device` 는 웹메일 기록에만 있고, User-Agent 가 없거나 알아보지 못하면 `null` 이다.
|
||||
- 끊은 세션 `DELETE /api/auth/sessions/{id}` 의 모양이 틀린 id 는 404 `RES_001`(없는 것과 같다).
|
||||
- 엔진 관리 화면에서 켠 2단계 인증은 `enabledAt` 이 없다.
|
||||
- 복구 코드가 없다 — 휴대폰을 잃으면 운영자에게 문의한다.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user