메일 서비스 ↔ HAL(hal21) 연동 실제 적용 기록: docs/verification/mail-hal-link-2026-10-27.md(호스트·인스턴스·백업 sha256·dry-run/apply/readback·Agent heartbeat·회귀·blocker), VERSION_HISTORY 결과 요약

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zzabisagent-lab
2026-10-27 05:35:12 +00:00
co-authored by Claude Fable 5.1
parent 79b45ca961
commit 4bd698452b
2 changed files with 68 additions and 1 deletions
+1 -1
View File
@@ -128,7 +128,7 @@
- **`hal/mailsvc.mjs`**(+ 마이그레이션 `hal_0003_mail_services`, `test/hal-mailimport.test.mjs` 7건): HAL 관리형 메일 서비스 등록부(`mail_services`, 도메인 단위 논리 리소스, 불변 id `mls_…`, 출처 권한 `arimail:<hostname>:<unit>`, 도메인별 active owner 정확히 하나 — 부분 유일 인덱스)와 가져오기 단위(`mail_imports`). 읽기 전용 discovery 문서만 입력으로 받고 **비밀값이 어디에든 있으면 거부**(비밀은 `secretRefs` 의 file:/dir:/vault:/env: 참조만 저장). dry-run 이 기본, 충돌(collision: 다른 서버/인스턴스가 이미 active)·중복·누락이 하나라도 있으면 아무것도 적용하지 않음, 적용은 한 트랜잭션 + 감사(immutable), 같은 출처·서버의 재적용은 update(멱등), 적용 단위 되돌리기(생성 행 삭제·갱신 행 복원). 소유 조직은 증거로 확인되기 전까지 `unassigned_legacy_managed`. Agent heartbeat 의 arimail 모듈 상태가 그 서버의 행에 `last_sync`·`health` 로 비친다(`core.heartbeat`). Data Plane(AriMail 송수신·계정·메일·DNS/MX·TLS·provider)은 바꾸지 않는다 — HAL 이 멈춰도 메일은 그대로다.
- **`scripts/mail-discover.mjs`**(+ `test/mail-discover.test.mjs` 2건): 원본 호스트에서 단독 실행하는 읽기 전용 discovery — 유닛 상태(`systemctl show`)·`current`/REVISION·package.json·AriMail 도메인 등록부(`data/domains.json`)·건강 API·haraka-status·게이트웨이/테넌트 DB **집계**(계정·메일 수·크기·무결성, 주소·내용 미열람)·유닛 PID 소유 리스너·nginx vhost·TLS 주체/만료·공개 DNS(1.1.1.1/8.8.8.8, 호스트 /etc/hosts 오염 방지)·certbot 훅. env/config 는 키 이름과 경로·권한만.
- **HAL CLI**(`hal/index.mjs`): `mail-import --discovery <json> (--server <id> | --create-server --name …) [--apply]`, `mail-import-rollback --import <id>`, `mail-services`(readback JSON), `server-token --server <id>`(운영자 콘솔이 닿지 않는 부트스트랩·연동 단계의 시스템 신원 토큰 발급, 감사 `cli:<by>`). **운영자 API**: `GET /ops/api/mail-services`(+readback: 총수·active·도메인 중복·서버별·비밀 누출 검사), `GET /ops/api/mail-services/:id`(감사 포함), `GET /ops/api/mail-services/imports`, `POST /ops/api/mail-services/import {discovery, serverId, apply}`(적용은 재인증), `POST /ops/api/mail-services/imports/:id/rollback`(재인증). **운영자 화면** `/ops/mail`: HAL 관리형 메일 서비스 표(도메인·공개 주소·서버/인스턴스·리비전·계정/메일함/메일·건강/동기화·TLS/DNS·비밀 참조 수)를 먼저 그리고 포털 메일 서비스는 그 아래 `#portal-mail` 에 중계로 채운다(`ops.js`).
- 실제 적용 결과·백업·readback 은 `docs/verification/mail-hal-link-2026-10-27.md`, 절차·되돌리기는 `docs/RUNBOOK-0.1.1.md` §9.
- 2026-10-27 실제 적용: bear HAL 을 커밋 79b45ca 릴리스로 교체(재시작 2.6초, 메일 서비스 프로브 무중단) → 서버 2(pending) · MailService 7(hostinger 인스턴스 6 도메인 + jitsi 1, 모두 `unassigned_legacy_managed`, duplicates 0, secretLeaks 0) · jitsi Node Agent 등록(heartbeat → `last_sync_source=agent`, 승인은 운영자 결정) · hostinger 는 기존 HAL(`hal.ariweb.net`) 제어 평면과의 이중 제어를 피해 Agent 미설치(관측 전용). 원본 메일 서비스 변경 0. 결과·백업 sha256·readback 은 `docs/verification/mail-hal-link-2026-10-27.md`, 절차·되돌리기는 `docs/RUNBOOK-0.1.1.md` §9.
### 도입 소스
- 가져오기: `node scripts/import-sources.mjs` 가 고정 커밋(Webmaker `3b10859884ef2fd0f2af11934ab6c16370fd64b8`, AriMail `a07f69f1ffb6667de6e66b3d1ec43c1c66fab924`)을 검사하고 실행에 필요 없는 문서·보고서·샘플을 제외한 전체를 `modules/webmaker`(176 파일)·`modules/arimail`(88 파일)로 복사, 파일별 원본 경로·대상 경로·수정 여부·원본 해시·라이선스 검토를 `IMPORTED_SOURCES.md`/`.json` 에 기록한다.
@@ -0,0 +1,67 @@
# 기존 메일 서비스 ↔ HAL(bear hal21) 연동 기록 — 2026-10-27 (Hera)
대상: `https://mail.arihakdang.com/` · `https://webmail.ibit.kr/` → bear HAL `https://hal21.ariweb.net` 의 관리형 MailService/Node 등록. 기존 송수신·계정·메일·DNS/MX·TLS·provider 는 바꾸지 않았다. 이 문서에 비밀값은 없다(경로·키 이름·해시만).
## 1. 소스·배포 리비전
| 항목 | 값 |
|---|---|
| 작업 시작 시 origin/main | `ac072e6f12a910ccd60dc7e63a9724363617a46b` (rebase 뒤 `03cb9c0777ddc3dc2003893d3dddd53a01565f2b`) |
| 코드 커밋(배포한 HAL) | `79b45ca96102213d2bd30856ef79b6d732758349` — release.mjs build: testsPassed=true, dirty=false, hal artifact sha256 `bf636f847266e319c888f3ba9f40fc8a22c9f5e6f43775d61d967752742db489` |
| bear HAL 배포 전 | `/srv/ariweb/hal/releases/0.1.1-ac072e6f12a9` (hal.sqlite servers 0 · agents 0 · ops_accounts 1 · mail_services 표 없음) |
| bear HAL 배포 후 | `/srv/ariweb/hal/releases/0.1.1-79b45ca96102` → `current`, `ariweb-hal` 05:28:55Z 재시작(재시작→/api/health 2.6초, NRestarts 0), `/api/health.bootstrap.mainCommit=79b45ca…`, 릴리스 0.1.1 artifact 재등록(approved, by hera-mail-onboard) |
## 2. 실제 호스트·인스턴스(읽기 전용 조사)
| | mail.arihakdang.com | webmail.ibit.kr |
|---|---|---|
| 호스트 | hostinger `srv1953374` (187.53.133.173, Ubuntu, Node v24.14.1 /opt/arihakdang-node) | `jitsi.ibit.kr` (115.68.194.192, Ubuntu 22.04, Node v22.23.3 /opt/arimail-node) |
| 유닛 / 실행 계정 / cwd | `arihakdang-mail.service` (+`-haraka`) / arihakdang / `/srv/arihakdang-mail/current` | `arimail.service` (+`-haraka`) / arimail / `/srv/arimail/current` |
| 리비전 | `releases/0.1.1` REVISION.json: Suite arimail 0.1.1, upstream AriMail 1.0.11, 2026-10-26 설치(hostinger HAL 모듈 배포) | `releases/20261024-204656` REVISION `a07f69f1ffb6667de6e66b3d1ec43c1c66fab924`, package 1.0.11 |
| 설정·비밀 참조 | `/etc/arihakdang-mail/gateway.env`(0600) · `data/config.json`(0600) · `/etc/arihakdang-mail/tls/` · `data/setup-token` | `/etc/arimail/gateway.env`(0600) · `data/config.json`(0600) · `/etc/arimail/tls/` · `data/setup-token` |
| 도메인 등록부 | 6: arihakdang.com(primary, auth www) · ariweb.net(webmaker) · check/solo-mvpcgayc·mvpcu283.ariweb.net(4개, Suite E2E 테넌트, operating=frozen, hostinger 포털 group/mailService 링크) | 1: ibit.kr(primary, auth local) |
| 집계(계정=메일함 / 메일 / 저장) | 10 / 195 / 30.4MB(DB 기준; data 디렉터리 545MB — 과거 pre-* DB 백업 포함) | 3 / 0 / 2.1MB |
| 리스너(유닛 PID 소유) | 110·143·465·587·993·995(0.0.0.0), 8790·8791(127.0.0.1). 25 없음 | 110·143·465·587·993·995, 8790. 25 없음 |
| provider | Resend(inbound/outbound), Haraka 설치됨·비활성 | Resend, Haraka 비활성. journal: `resend._domainkey.ibit.kr` TXT 검증 실패 반복(기존 상태, 변경 없음) |
| DNS(공개 리졸버) | A 187.53.133.173 · MX arihakdang.com→inbound-smtp.ap-northeast-1.amazonaws.com | A 115.68.194.192 · MX ibit.kr→inbound-smtp.ap-northeast-1.amazonaws.com |
| TLS | LE `CN=mail.arihakdang.com`(+SAN webmail.ariweb.net) 만료 2027-01-12 | LE `CN=webmail.ibit.kr` 만료 2027-01-19 |
| nginx / certbot 훅 | `sites-available/mail.arihakdang.conf` → 127.0.0.1:8790, 80→301 https · deploy 훅 `arihakdang-mail.sh`(갱신 시 유닛 재시작) | `webmail.ibit.kr.conf` → 127.0.0.1:8790, 80→301 · 훅 `arimail.sh` |
| 기존 제어 평면 | hostinger HAL(`hal.ariweb.net`, 부트스트랩 완료) `module_targets.arimail` 0.1.1 ok, Agent `ag_01a178829f…` 등록 | 없음 |
두 URL 은 **서로 다른 호스트·다른 AriMail 인스턴스**다(같은 멀티도메인 리스너가 아님).
## 3. 사전 스냅샷(비밀 제외 sha256, 모두 root 0700/0600, git 밖, `sha256sum -c` 통과, SQLite `integrity_check=ok`)
| 위치 | archive.tgz | db.tgz | code.tgz |
|---|---|---|---|
| hostinger `/srv/arihakdang-mail/backups/pre-hal-onboard-20261027T051350Z/` (RESTORE.md 포함) | `abb731f26e6a06c4…` (32 entries: data·config·domains.json·gateway.env·유닛·nginx·훅) | `1ba2eb9c2ba6c8ad…` (VACUUM INTO 사본 6개: 게이트웨이 + 테넌트 5) | `releases/0.1.1` 트리 106 entries |
| jitsi `/srv/arimail/backups/pre-hal-onboard-20261027T051352Z/` | `7e36bcbff719dc36…` (20 entries) | `26035be7a4afdc71…` (사본 1) | `releases/20261024-204656` 66 entries |
| bear `/srv/ariweb/backups/hal-pre-mail-onboard-20261027T051353Z/` | `534c9eb1228c011e…` (vault·config·유닛·nginx) | `da848bc05c84ef7d…` (hal.sqlite 사본, servers 0/agents 0/ops_accounts 1) | `releases/0.1.1-ac072e6f12a9` 79 entries |
복원 순서는 각 디렉터리의 `RESTORE.md`(메일 복원은 서비스 중단 동반 → 운영자 승인 뒤에만). HAL 되돌리기: `mail-import-rollback --import <id>` 또는 RESTORE.md(hal.sqlite 사본 + current 심링크 `0.1.1-ac072e6f12a9`).
## 4. discovery → dry-run → apply → readback (bear, HAL CLI, 시스템 신원 `cli:hera`)
- discovery 문서: `scripts/mail-discover.mjs`(sudo, 읽기 전용) — sha256 `1cfb2042138d1de3…`(hostinger, 05:22:06Z) · `18cda7182ac68dd7…`(jitsi, 05:22:07Z). `validateDiscovery` ok(비밀값 0, 메일 주소 0).
- 서버(노드) 생성: `srv_01a17bd4dbcdb55877f2d9fcab` "srv1953374 (mail.arihakdang.com)" role mail · `srv_01a17bd4dc2a2bc3050054fee0` "jitsi.ibit.kr (webmail.ibit.kr)" role mail — 둘 다 pending.
- dry-run: `mimp_01a17bd4dbd076ebd300d5a681`(create 6·reject 0) · `mimp_01a17bd4dc2e21a185a4cefc09`(create 1·reject 0). DB 변경 없음(mail_services 0).
- apply: `mimp_01a17bd5f2685f8929d7638a28` → 생성 6(`mls_01a17bd5f26934dd18266db83a` arihakdang.com, `…f26a6e17e69ff81adb` ariweb.net, `…f26afc33723ce9c622` check-mvpcgayc, `…f26a1478f1bafcdca7` solo-mvpcgayc, `…f26aa7eab73a48883e` check-mvpcu283, `…f26bbb6aa9e045b23b` solo-mvpcu283) · `mimp_01a17bd5f2eaf5667cbc127dac` → 생성 1(`mls_01a17bd5f2eaf032215343d7cb` ibit.kr).
- 멱등·충돌 검증: 같은 문서 재적용 `mimp_01a17bd5f35c29928b0609ec45` = update 6·create 0(id 유지) · jitsi 문서를 hostinger 서버에 적용 시도 `mimp_01a17bd5f3c482c2797e178e46` = rejected(ibit.kr collision) — 부분 적용 없음.
- readback(`hal/index.mjs mail-services`, 05:33Z): total 7 · active 7 · 도메인 7(각 1) · duplicates [] · secretLeaks 0 · perServer {hostinger 6, jitsi 1} · 모두 kind legacy_managed · ownership **unassigned_legacy_managed**(ariweb.net 테넌트의 hostinger 포털 group 링크는 domain_json 증거로만 보관, 매핑하지 않음). 각 행: 서버·유닛·appDir·리비전·집계·리스너·TLS 만료·DNS MX·nginx·secretRefs(참조 4건, 값 없음).
- 감사: `server.create` 2 · `mail_service.import_dry_run` 2 · `mail_service.import` 13(immutable) · `mail_service.import_rejected` 1 · `release.register/approve`. hal.sqlite `integrity_check=ok`, 마이그레이션 `hal_0003_mail_services` 적용(기존 표 변경 없음).
## 5. Node Agent / heartbeat
- jitsi: 토큰 `hal/index.mjs server-token --server srv_01a17bd4dc2a2…`(1회, 화면·로그 미기록) → `https://hal21.ariweb.net/agent/v1/install.sh --role mail --label jitsi.ibit.kr`. 결과 Agent `ag_01a17bd7201c9a73c157e29968`(managed, 05:30:58Z), 유닛 `ariweb-agent`(master, `/srv/ariweb/agent/current` 0.1.1) active NRestarts 0, 비밀은 `/srv/ariweb/agent/data` 보관소만. 설치가 만든 것: `/srv/ariweb/agent`, `/srv/ariweb/backups`, `/var/www/ariweb-sites`, `/etc/systemd/system/ariweb-agent.service`, `/etc/sudoers.d/ariweb-agent`, `/etc/nginx/conf.d/awstats.conf`(log_format, `nginx -t` 성공·graceful reload). **arimail 유닛·PID·리스너 변화 없음**(PID 3304850 / 3304871 그대로).
- heartbeat readback(05:32:59Z): servers.last_seen 갱신, agent_version 0.1.1, metrics_samples 5건, module_targets `arimail actual 1.0.11 health ok`(REVISION.json 없음 → package.json), 시계 차이 25,053ms(경고 기준 60초 미만 — 호스트 시각 확인 권장). `mail_services`(ibit.kr) `last_sync_source=agent`, health ok, observedVersion 1.0.11.
- hostinger: hal21용 Agent **설치하지 않음** — 기존 Agent 가 hostinger HAL 에 등록되어 있고 같은 유닛을 두 제어 평면이 배포하면 §34 "하나의 HAL" 위반. 서버 행은 pending(Agent 미연결), 메일 서비스 6건은 `last_sync_source=import`(관측 전용). 제어 평면 이전은 사람 결정.
- 서버 승인은 하지 않았다(첫 Agent 승인이 부트스트랩을 닫고 수동 설치를 잠근다 — 운영자 결정).
## 6. Data Plane 독립·회귀(적용 뒤)
- HAL 재시작 구간(05:28:5x) 동안 외부 프로브: `mail.arihakdang.com /api/health` 200 · `webmail.ibit.kr /api/health` 200 · IMAPS 993 `* OK` 연속 유지(12회, 실패 0). AriMail 유닛은 HAL 을 참조하지 않는다(EnvironmentFile·유닛 파일 sha256 변화 없음).
- 두 호스트 사후 점검: 유닛 active/enabled, NRestarts 0, 시작 시각·PID 변화 없음(hostinger 1046648/1046634, jitsi 3304850/3304871) · 리스너 동일 · `/api/health` {ok, 1.0.11, domains 6 / 1} · DB 계정/메일 수 동일(2+2+2+1+2+1 / 189+6, 3 / 0) · gateway.env·config.json·domains.json·유닛·nginx vhost·fullchain.pem sha256 변화 없음 · `nginx -t` 성공 · journal 60분 오류 0(hostinger) / 1(jitsi, 기존 Resend DKIM TXT 검증 실패 반복) · outbound_queue 0 · Haraka 비활성 그대로.
- 외부 프로토콜(05:33Z): 두 호스트 모두 http→https 301, https 200, 587 STARTTLS·465·993·995 인증서 일치(verify 0), IMAPS `* OK`·POP3S `+OK`·SMTPS `220` greeting, 25 닫힘, MX/A 변화 없음.
- hostinger HAL 제어 평면 변화 없음(`module_targets.arimail` 0.1.1 ok, Agent 1051076 active).
- HAL 외부: `/api/health` 200, `/status` 200, `/` → `/ops/` 302, `/ops/mail` 200(로그인 화면), `/ops/api/mail-services` 401(운영자 세션 필요), 인증서 `hal21.ariweb.net` 2027-01-25.
## 7. 테스트
`node test/run.mjs`: 21 파일 144 건 통과·실패 0(신규 `hal-mailimport` 7, `mail-discover` 2). `bash -n deploy/*.sh` 통과(shellcheck 미설치). `scripts/release.mjs build/verify` 통과.
## 8. 바꾸지 않은 것 · blocker · 미확인
- 바꾸지 않음: DNS/A/MX/TXT, 인증서, AriMail 코드·유닛·env·config·domains.json·DB·메일·계정, 리스너, Haraka 상태, hostinger HAL/포털/Agent, bear nginx. 테스트 메일 발송·임시 계정 생성 없음.
- blocker(사용자 조작 필요): (1) hal21 운영자 자격 증명 없음 → 운영자 API/콘솔 readback·서버 승인·접근권 발급은 수행하지 않았다(readback 은 HAL DB/CLI 로 수행). (2) hostinger 의 제어 평면(hal.ariweb.net ↔ hal21) 결정 전에는 hostinger Agent 를 hal21 에 등록하지 않는다. (3) 4개 E2E 테넌트(frozen)와 ariweb.net 의 소유 조직 매핑은 포털 그룹 대조 뒤 운영자가 지정.
- 미확인: jitsi 시계 차이 25초(원인 미조사), ibit.kr Resend DKIM TXT 미검증(기존).