Skip to content

Ажиллагаа

Байршуулалт ба платформын ажиллагааны дүрмүүд. .github/workflows/deploy-dev.yml ба ажиллаж байгаа суулгацаас уншиж бичигдсэн.

Баримтын төв · Архитектур · Гарын авлага


Хаалттай хэрэглэгчийн бүртгэл

Gerege Nexus нь Gerege Eco System-ийн хаалттай суурь платформ. platform.access_mode=private тохиргоо registry.platform_settings хүснэгтэд хадгалагдана. Энэ нь .env хувьсагч биш бөгөөд deploy хийхэд хэвээр үлдэнэ. GET /api/v1/auth/sso/config хариуны access_mode нь private байх ёстой.

eID нь хүнийг таньж баталгаажуулна. Нэвтрэх эрхгүй хэрэглэгчид байгууллагын админ урилга эсвэл урьдчилсан бүртгэлээр эрх олгоно. Нэг хүнийг нэвтрүүлэхийн тул платформыг public горимд шилжүүлэхгүй. Операторын консолын эрх болон TOTP тохиргоо тусдаа хэвээр байна.

Үйлчилгээний хаягууд

Нэр Юу Ард нь
nexus.gerege.net Платформ өөрөө — frontend ба /api/v1/* Next.js + Go
admin.nexus.gerege.net Операторын консол — /api/platform/v1/* Мөн тэр хоёр
docs.nexus.gerege.net Энэ суулгацын баримт Статик, MkDocs — docs/mkdocs/deploy.sh <ssh-host>

Хоёр урсгал өөр origin дээр байгаа нь гоо зүйн биш: cookie нь hostname-аар хязгаарлагддаг тул тусдаа нэр нь тусдаа ambient authority гэсэн үг.

Платформын нүүр хуудас эдгээрийг карт болгож харуулна. Хаягууд нь кодод биш, суулгацын орчинд (SERVICE_URL_ADMIN, _DWH, _BACKUPS, _MONITOR, _DOCS, мөн үндэсний _EID): admin.nexus.gerege.net бол энэ суулгацынх, өөр хэн ч биш. Тохируулаагүй үйлчилгээ зурагдахгүй, нэг ч тохируулаагүй бол хэсэг бүхэлдээ зурагдахгүй.

Консолын хаалга нь мөн платформын landing дизайнтай: нэвтрэх маягт нь hero дотор, доор нь эрхийн загвар ба impersonation-ий нөхцөлүүд. Session-гүй ирсэн хүн ард нь юу байгааг мэдэлгүй үлдэх ёсгүй — тэр хаяг юу болохыг хэлэх нь нууц задруулах биш, нэвтрэх шалгалт нь ард нь хэвээр байна.

Байршуулалт

Release хийх, deploy-ийн хугацаа, унавал юу хийх дүрэм: DEVELOPMENT.md — Release ба deploy.

Цорын ганц суулгац: https://nexus.gerege.net (консол admin.nexus.gerege.net), owner-ийн Proxmox core-dev pool дээр. nexus.gerege.mn 2026-10-25-нд хаагдсан.

  • main руу push хийхэд CI ажиллана. Image-уудыг CI (ci.yml, images ажил) тестүүдтэй зэрэг НЭГ удаа барьж, GHCR руу commit-ийн SHA-гаар түлхэнэ.
  • CI ногоон болмогц deploy-dev.yml self-hosted core-dev runner дээр яг тэр SHA-ийн image-уудыг app host дээр татаж, docker compose up -d-ээр гаргаад /health ба нүүр хуудсыг (3 мин), дараа нь https://nexus.gerege.net-ийг шалгана. Өөр SHA-г гаргах бол gh workflow run deploy-dev.yml -f tag=<SHA>.
  • Compose файл, нууцууд (.env) ба нөөцлөлт энэ репод биш: proxmox-gerege-net (stack/nexus). Тохиргоог тэндээс өөрчилнө.

Хувилбар нь image дотор ldflags-аар шатаагдана. Тэгэхгүй бол суулгац бүр өөрийн хувилбарыг мэдэхгүй болно — каталог "platform": ">=1.1.0" шаардахад хариулах зүйлгүй.

Миграцын дүрэм: нэг хувилбарын турш хойш нийцтэй

Migrate-ийн дараа шинэ backend бэлэн болох хүртэл хуучин binary шинэ схем дээр ажиллана, буцаахад ч мөн адил. Тиймээс миграц бүр expand/contract хэлбэртэй:

  • Expand (энэ хувилбар): багана/хүснэгт нэмэх, nullable эсвэл default-той; хуучин код түүнийг мэдэхгүй ч эвдрэхгүй.
  • Contract (дараагийн хувилбар): хуучин код ашиглахаа больсны ДАРАА л устгах, нэрлэх, NOT NULL болгох.

Жишээ — users.name-ийг display_name болгох:

-- 00091 (хувилбар N): нэмэх, хуулах. N-1 нь `name`-ийг уншсаар.
ALTER TABLE users ADD COLUMN display_name VARCHAR(200);
UPDATE users SET display_name = name WHERE display_name IS NULL;
-- Код N: хоёуланд нь бичиж, display_name-ийг уншина.

-- 00095 (хувилбар N+1): N нь `name`-ийг уншихаа больсон тул одоо л хасна.
ALTER TABLE users DROP COLUMN name;

Шууд RENAME COLUMN эсвэл DROP нь хуучин backend-ыг солигдох хүртэл 500 болгоно. API нь эхлэхдээ өөрийн миграцын хамгийн сүүлийн хувилбарыг өгөгдлийн сангийнхтай харьцуулж, схем хоцорсон бол /ready 503 буцаана (урагшилсан бол үгүй — хуучин хувилбар бэлэн хэвээр байх ёстой). Харьцуулалт нь зөвхөн цөмийн миграц (db/migrations, өөр байрлалтай бол CORE_MIGRATIONS_DIR) ба public.goose_db_version-ийг хардаг. MIGRATIONS_DIR / MIGRATIONS_TABLE нь зөвхөн cmd/migrate-ийнх (distribution-ий өөрийн миграц) — API-д өгсөн ч энэ шалгалтад нөлөөлөхгүй. API-ийн нэвтрэх роль (API_DATABASE_URL) public.goose_db_version-ийг унших эрхгүй (42501) бол шалгалт өөрөө унтарч, логт нэг удаа schema version check is off гэж WARN бичнэ; /ready үүнээс болж 503 болохгүй. Шалгалтыг буцааж асаах бол тэр рольд SELECT эрх өг.

Tenant isolation-ий нөхцөл: /ready ба бусад route нь dbguard-ийн probe дуусах хүртэл 503. Бодлого/роль огт суугаагүй өгөгдлийн сан (probe амжилттай, guard асаагүй) нь хэвийн төлөв тул үйлчилнэ; зөвхөн probe унасаар/хүлээгдэж байвал хаалттай үлдэнэ.

Модулийн миграц (API эхлэхэд appinstall) нь pool-оос биш тусдаа холболтоор statement_timeout=0, lock_timeout=10s-тэй ажиллана: удаан CREATE INDEX тасрахгүй, харин хуучин ба шинэ API давхцах үед түгжээ 10 секундээс удаан хүлээвэл унаж (уншигчдыг түгжээний дараалалд удаан барихгүйн тулд), дараагийн эхлэлт/суулгалтаар дахин оролдоно. Байгууллага устгах sweep ба өдрийн metering нь өөрийн транзакцад SET LOCAL statement_timeout = '5min' тавина.

Миграц унавал. goose миграц бүрийг өөрийн транзакцад ажиллуулдаг тул унасан файл өөрөө буцна, харин түүнээс өмнөх энэ deploy-ийн файлууд хэрэгжсэн хэвээр. Ажиллаж буй өгөгдлийн сан руу автоматаар pg_restore хийхгүй.

Ажиглалт

Ажиглалтын стек (Prometheus, Loki, Grafana, Alertmanager) хаагдсан nexus.gerege.mn-тэй хамт энэ репоос хасагдсан. Платформ өөрөө доорх хэмжүүрийг гаргасаар байна.

Хэмжүүр

Хэмжүүр бүр OpenTelemetry-ийн SDK-аар дамжиж, Prometheus exporter-ээр /metrics дээр гарна. HTTP-ийнх нь semantic convention-ыг дагана:

http_server_request_duration_seconds{http_request_method,http_route,http_response_status_code}

Хүсэлтийн тоо нь тусдаа counter биш — тэр гистограммын _count цуврал.

http.request.method нь хаалттай олонлог. Танихгүй үйл үг _OTHER болж нугалагдана: PROPFIND гэж дуудсан сканнер time series үүсгэх ёсгүй.

Гадаад систем бүр нэг histogram-аар хэмжигдэнэ — external_request_duration_seconds{system,operation,status} — системийн нэр нь мөн адил хаалттай олонлог, танихгүй нь other.

Exemplar

Trace асаалттай үед гистограммын сэмпл бүр trace_id авч явна. Латенси графикийн удаан цэг дээр дарахад Grafana тэр яг тэр хүсэлтийн trace-ыг нээнэ — «аль хүсэлт удаан байсан бэ» гэдгийг таамаглах хэрэггүй.

/metrics нь OpenMetrics хэлбэрээр үйлчилдэг байх ёстой, эс бөгөөс exemplar дамжихгүй.

Нөөцлөлт

nexus.gerege.net-ийн нөөцлөлт энэ репод биш — proxmox-gerege-net (stack/nexus).

Олон байгууллагыг нэг дор нээх

Консолын маягт нэгийг нээдэг. Суулгац эхлэх өдөр — зохицуулагч хоёр зуун лицензтэй компанийг, яам харьяа агентлагуудаа оруулах — тэр нь ажлын урсгал биш, хэн ч зарцуулахгүй үдэш болно. Setup wizard ч тусалдаггүй: тэр нь эхний байгууллагыг л нээдэг.

POST /api/platform/v1/tenants/bulk
{"organisations":[{"name":"…","slug":"…","admin_email":"…","reason":"…"}, …]}

Гурван шинж чанар, ач холбогдлын дарааллаар:

  • Гүйлгээ БИШ. 137 дахь мөр унасан нь өмнөх 136 байгууллагыг устгах ёсгүй. Мөр бүр өөрийн аудитын бичлэгтэй, өөрийн үйлдэл — оператор маягтыг 200 удаа илгээсэнтэй яг адил.
  • Мөр бүр тайлагнана. Хариу нь тоо ч биш, эхний алдаа ч биш, үр дүнгийн жагсаалт. 200 мөр илгээсэн хүн алийг нь буугаагүйг, яагаад гэдгийг мэдэх ёстой.
  • Дахин илгээх нь аюулгүй. Байгаа slug нь exists, алдаа биш. Файл засагдаад дахин илгээгддэг; хоёр дахь ажиллуулалт нь шинэ хоёр мөрийг нуусан алдааны хана байвал гурав дахь удаа хэн ч илгээхгүй.

Нэг хүсэлтэд дээд тал нь 200 байгууллага. Энэ нь гүйцэтгэлийн хязгаар биш, дэлбэрэлтийн радиус: дуудагч талын давталтын алдаа нь татгалзсан хүсэлт байх ёстой болохоос, тус бүрдээ хоёр superadmin шаардах дөрвөн мянган байгууллага биш.

CSV, Excel, файл байхгүй. Консол JSON ярьдаг бөгөөд компанийн жагсаалт барьсан хүн түүнийг хөрвүүлж чадна; энд задлагч бичих нь кодчиллын алдаа амьдрах хоёр дахь газар болох бөгөөд доор нь энэ endpoint хэвээр хэрэгтэй.

Клиентийн хаяг ба консолын хаягийн жагсаалт

security.ClientIP (rate limiter, аудит, консолын хаягийн шалгалт) нь X-Real-IP / X-Forwarded-For-г хоёр нөхцөл зэрэг биелэхэд л уншина: TRUST_PROXY_HEADERS=true, мөн хүсэлт ирсэн peer (RemoteAddr) TRUSTED_PROXY_CIDRS дотор байх. Анхдагч нь loopback (127.0.0.0/8, ::1). Өмнө нь эхний нөхцөл л хангалттай байсан тул API-ийн socket-д шууд хүрсэн хэн ч өөрийн хаягаа сонгож, login limiter болон консолын allowlist-ийг тойрч чаддаг байв.

Docker-ийн урхи. nginx нь хост дээр 127.0.0.1:<порт> руу proxy хийдэг ч контейнер доторх API нь тэр холболтыг Docker bridge-ийн gateway хаягаас (172.x.0.1) ирсэн гэж хардаг — loopback биш. Тиймээс compose-оор ажилладаг суулгацад TRUSTED_PROXY_CIDRS-г тухайн compose сүлжээгээр (эсвэл 172.16.0.0/12) тавих ёстой; эс бөгөөс бүх хэрэглэгч нэг хаяг болж, login limiter бүгдийг хамт хаана. Буруу үед API нэг удаа ignoring forwarding headers from a peer outside TRUSTED_PROXY_CIDRS гэж ERROR лог бичнэ. Хувьсагч контейнерт хүрэхийн тулд compose файлын backend environment:-д TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-} мөр байх ёстой.

Консолын хаягийн жагсаалт (CONTROL_PLANE_ALLOWED_CIDRS) нь платформын access mode-оос хамааралгүй болсон: жагсаалт байвал public ч, private ч үед шалгана. Production-д жагсаалт хоосон бол консол хаалттай (404) — CONTROL_PLANE_ALLOW_ALL=true эсвэл CONTROL_PLANE_ALLOWED_CIDRS=open гэж илт бичсэн үед л нээлттэй. Хаалттай үед нэг удаа production has no console allowlist гэж ERROR лог гарна.

Суулгацын .env-д (proxmox-gerege-net):

TRUSTED_PROXY_CIDRS=172.16.0.0/12
CONTROL_PLANE_ALLOWED_CIDRS=<операторуудын сүлжээ>   # эсвэл: open

Операторын TOTP нууц

operator.operator_accounts.totp_secret нь INTEGRATION_ENCRYPTION_KEY-ээр битүүмжлэгдэж (sealed:v1: угтвар) хадгалагдана. Өмнөх plaintext мөрүүд хэвээр уншигдаж, дараагийн нэвтрэлт дээр автоматаар битүүмжлэгдэнэ — SQL migration түлхүүр мэдэхгүй. Production-д түлхүүргүй бол шинэ оператор бүртгэхээс татгалзана. Түлхүүрийг солих нь бүх операторын TOTP-г уншигдахгүй болгоно — operator-bootstrap командаар дахин бүртгэнэ.

Консол нэвтэрсний дараа үйлдэл бүрт TOTP код (step-up) ба шалтгаан асуухгүй (2026-10-04); хамгаалалт нь нэвтрэлтийн хоёр хүчин зүйл ба үүргийн эрх. /me/password нь IP тутамд rate limit-тэй. Нууц үг солиход тухайн операторын бусад бүх session нэг транзакцид цуцлагдана.

eID нэвтрэлт: хариуны баталгаажуулалт ба flow token

Хариуны баталгаажуулалт. Зөвшөөрөгдсөн eID session-ийн ACSP_V2 гарын үсэг ба гэрчилгээний гинжийг EID_VERIFY_RESPONSE удирдана. EID_RP_UUID тавьсан production API нь enforce-оос өөр утгатай бол асахгүй (логт EID_VERIFY_RESPONSE must be enforce in production). Шалгалт амжилтгүй approval-ыг enforce 502-оор татгалзаж, иргэн нэвтрэхгүй; warn-д VerifiedStatus=false болж зөвхөн warn зөвшөөрөгдсөн суулгацад нэвтэрнэ.

Шилжих дараалал:

  1. Одоогийн prod лог дээр approved session did not verify мөр байгаа эсэхийг шалга. Байхгүй бол шууд EID_VERIFY_RESPONSE=enforce.
  2. Байвал (CA-ийн гинж embedded-ээс өөр) EID_ROOT_CA_PEM/EID_ROOT_CA_FILE-оор anchor нэм, эсвэл түр EID_VERIFY_RESPONSE=warn + EID_VERIFY_RESPONSE_ALLOW_WARN=true (асах бүрд ERROR лог).
  3. Лог цэвэр болсны дараа enforce, ALLOW_WARN-ыг устга.

off production-д ямар ч override-оор зөвшөөрөгдөхгүй.

Иргэний гэрчилгээ олгогч (issuer allowlist). Гинж үндэсний root руу хүрэх нь хангалтгүй: eID Mongolia-ийн Organization Issuing CA (05) ижил root дор clientAuth гэрчилгээ (RP mTLS, байгууллагын төлөөлөгч) олгодог. Тиймээс гэрчилгээг шууд олгосон CA-ийн Subject Key Identifier allowlist-д байх ёстой; үгүй бол enforce татгалзана (логт the certificate's issuer "…" is not a citizen sign-in CA). Анхдагч — зөвхөн eID Mongolia Issuing CA (04, b8a37f2c64ad677912db7d9e34f246358a2fb909); rp.eidmongolia.mn руу заасан суулгацад юу ч тохируулах шаардлагагүй. Өөр CA (staging г.м.) руу заасан distribution EID_ROOT_CA_PEM/_FILE-тэй хамт EID_CITIZEN_ISSUER_SKIS-д тэр CA-ийн иргэний issuing CA-ийн SKI-г заавал тавина (таслалаар, hex, : зөвшөөрнө; анхдагчийг солино — хоёуланг нь хүсвэл хоёуланг нь жагсаа): openssl x509 -in issuing.pem -noout -ext subjectKeyIdentifier. Буруу утга ERROR лог бичээд анхдагчаар үргэлжилнэ. Эхлэх лог: eid: response verification … anchors=1 intermediates=4 citizen_issuers=1. Embedded trust store (backend/internal/kernel/eidrp/trust/) нь CA репогийн pki/chain/ 01–05; хуучин self-signed ба DPoD-ийн issuing CA-ууд 2026-09-26-нд хасагдсан (тэдгээрээр олгосон амьд гэрчилгээ 0).

Device link (EID_DEVICE_LINK_MODE). v3 (анхдагч, 2026-10-02-оос) — eID Mongolia хуучин статик …/dl?sessionId=…&vc=… холбоосыг 2026-10-02-нд унтраасан (апп DEVICE_LINK_REQUIRED, session TIMEOUT болно — developer.eidmongolia.mn §4.2). legacy — тэр статик холбоос, зөвхөн түүнийг дэмждэг CA-д. v3 — Smart-ID v3 динамик холбоос (ca-eidmongolia-mn docs/DEVICE_LINK_V3.md): QR нь elapsedSeconds + authCode (HMAC-SHA256, CA-ийн sessionSecret)-тэй, дэлгэц POST /api/v1/auth/eid/qr (консол: POST /api/platform/v1/session/eid/qr)-оос секунд тутам шинээр авна; утсан дээрх товч нь Web2App холбоос — зөвхөн callback-тай session-д, тиймээс утсан дээрх binding (/auth/bind/eid/start), профайлын холбох (/profile/identities/eid/start) ба консол (/api/platform/v1/session/eid/start {"same_device":true,"browser":"retScheme=…"}) v3 горимд callback илгээнэ (legacy-д үл тооно, callback-гүй хэвээр). Бүгд PUBLIC_ORIGIN/auth/eid/callback (CA-д бүртгэлтэй ганц host) руу буцна; тэр хуудас note-ийн төрлөөр (sign-in / link / bind) зохих дуусгалтыг userChallengeVerifier-тэй асууна; консол ?to=cp тэмдэгтэй буцаж https://$CONTROL_PLANE_HOST/cp/eid/callback руу (зөвхөн sessionId + verifier) дамжина. Санал болгосон flowType = олгосон холбоосын төрлүүд (QR; + Web2App/App2App). sessionSecret нь registry.eid_rp_pending-д INTEGRATION_ENCRYPTION_KEY-ээр битүүмжлэгдэж хадгалагдана — browser/лог руу хэзээ ч гарахгүй; legacy горимд ч хадгалагдана (шилжихээс өмнө битүүмжлэл ажиллаж буйг харах; амжилтгүй бол the v3 device-link fields could not be kept WARN). v3 холбоосыг eID Mongolia апп 2.2.3-аас уншина. Эхлэх лог: eid: device links mode=….

RP mTLS (rp.eidmongolia.mn). eID Mongolia нь RP бүрд require_mtls асааж болно — тэгвэл Bearer secret-ээс гадна CA-ийн олгосон client cert шаардана. Cert-ийг CA-ийн admin sign-csr (profile=rp, Organization CA, EKU clientAuth) олгоно; түлхүүр хост дээр үүсэж хэзээ ч гадагш гарахгүй. Хосыг compose-ийн хажуугийн ./eid-client/ (контейнерт /run/eid-client, read-only; түлхүүр image-ийн хэрэглэгчид уншигдах) хавтаст тавьж EID_CLIENT_CERT_FILE + EID_CLIENT_KEY_FILE, EID_BASE_URL=https://rp.eidmongolia.mn (/v3-гүй — 2026-09-29-өөс тэнд 404). Хос нь handshake бүрд уншигдана (сунгасан cert restart-гүй орно); production нэг нь дутуу, уншигдахгүй эсвэл хугацаа дууссан хостой асахгүй.

Flow token. session_id QR код дотор ил тул дангаараа eID session-ийг дуусгах эрх олгохгүй. /auth/eid/start, /auth/eid/start-id, /auth/bind/eid/start, /profile/identities/eid/start, консолын /session/eid/start нь санамсаргүй токен өгнө: хөтөчид HttpOnly gerege_eid_flow cookie (/api/v1, консолд /api/platform/v1), native клиентэд хариуны flow_token. Дуусгах бүх endpoint (/auth/eid/poll, /auth/bind/eid/poll, /profile/identities/eid, /session/eid, /me/eid) токен шаардаж, registry.eid_flows (00118) дахь мөрийг нэг удаа зарцуулна — давтан poll 403. Токенгүй poll CA-аас асуухаас өмнө татгалзагдана (RUNNING эсэхийг ч мэдэхгүй) — ажлын талд ч, консолын /session/eid, /me/eid дээр ч. Профайлаас eID холбох session-ийг зөвхөн /profile/identities/eid/start эхлүүлнэ (утсан дээр «eID холбох: <данс>»); flow нь link:<session_id> түлхүүрээр бичигддэг тул нийтийн /auth/eid/start-аар эхэлсэн («нэвтрэх» гэж уншигддаг) session-ийг /profile/identities/eid хэзээ ч дуусгахгүй. Native аппын хуучин build (flow_token буцааж илгээдэггүй) eID-ээр нэвтэрч чадахгүй — энэ өөрчлөлтийг deploy хийхээс өмнө шинэ build-ийг тараа.

Binding cookie. Google-ээр анх нэвтэрсэн хүний /login/bind?b=<token> холбоос дангаараа binding-ийг нэрлэхгүй: callback нь токены SHA-256-г HttpOnly gerege_bind cookie-д (/api/v1/auth/bind, 20 мин, production-д Secure, SameSite=Lax) тавьдаг ба /auth/bind/* дөрвөн endpoint хоёулаа таарахыг шаардана — өөр хөтчөөс ирсэн токен 404. Poll амжилттай дуусахад cookie арилна.

Регистрээр push (/auth/eid/start-id, /auth/bind/eid/start) нь нэг регистрт нэг IP-ээс 10 минутад 3, бүх IP-ээс нийт 10 удаа (Redis байвал бүх replica-д нийт). Өмнө нь регистр тутамд 3 байсан тул регистр мэдэх хэн ч иргэний push нэвтрэлтийг хааж чаддаг байв (аудит №28); одоо нэг халдагч өөрийнхөө төсвийг л дуусгана.

Дансны түлхүүр (EID_ACCOUNT_KEY). eID дансны синтетик хаяг (eid+<digest>@identity.invalid) ба хуучин нууц үгийн шалгалт нь EID_ACCOUNT_KEY-ээр HMAC хийгдэнэ; тавиагүй бол EID_RP_SECRET (өмнөх зан төлөв, өөрчлөлтгүй). EID_RP_SECRET-ийг солихын өмнө EID_ACCOUNT_KEY-г одоогийн EID_RP_SECRET-ийн утгаар суулгацын .env-д тавь — эс тэгвэл eID холбоос мөргүй, синтетик хаягаар л олддог данс бүр дараагийн нэвтрэлтэд олдохгүй болж шинэ хоосон данс үүснэ (аудит №30). Тавьсны дараа EID_ACCOUNT_KEY-г хэзээ ч бүү соль.

Relying party-г консолоос (1.28.0-аас). EID_RP_UUID, EID_RP_SECRET нь консолын Тохиргоо → Түлхүүрүүд дахь eid.rp_uuid, eid.rp_secret болсон: консолын утга env-ээс давуу, нэвтрэлт, гарын үсэг, консолын eID клиентүүд утга өөрчлөгдөхөд дахин ачаалалгүй шинээр үүснэ. Production-д EID_VERIFY_RESPONSE=enforce (эсвэл warn + EID_VERIFY_RESPONSE_ALLOW_WARN) биш бол консолоос тавьсан relying party ажиллахгүй — boot-ийн шалгалттай ижил дүрэм. Дансны түлхүүрийн дараалал: EID_ACCOUNT_KEY → env-ийн EID_RP_SECRET → консолын нууц; консолоос л холбосон суулгацад ч нууцыг солихоос өмнө EID_ACCOUNT_KEY-г одоогийн утгаар тавь.

IP тутмын төсөв (Redis байвал бүх replica-д нийт): QR/App2App /auth/eid/start минутад 30, burst 20 — push илгээдэггүй, carrier NAT-ын ард олон иргэн нэг хаягтай; /auth/eid/start-id ба нууц үгээр нэвтрэх нь минутад 5, burst 5. Иргэний CA дахь өөрийн бүртгэл (/profile/eid/*, платформын RP credential зарцуулна) минутад 60, burst 20. Тоонууд auth.EIDStartRatePerMinute, auth.EIDRecordRatePerMinute, auth.LoginRatePerMinute.

eID гарын үсэг: хариуны баталгаажуулалт ба stamp

Баталгаажуулалт. eID-ийн endResult: OK-д дангаар нь итгэхгүй: PDF/digest-ийн гарын үсэг completed болохын өмнө eidrp.VerifySignedDigest гэрчилгээний гинж (нэвтрэлттэй ижил trust store — embedded + EID_ROOT_CA_PEM/_FILE + EID_CITIZEN_ISSUER_SKIS), emailProtection EKU + non-repudiation (SIGN profile — нэвтрэлтийн гэрчилгээ татгалзагдана), QUALIFIED түвшин, Nexus-ийн өөрийн хадгалсан SHA-256 digest дээрх гарын үсэг, PNOMN-<person.civilId> = subject serial, эхлүүлсэн иргэн (civil id эсвэл person.regNo)-ийг шалгана. Нэвтрэлтээс ялгаатай нь warn/off горим байхгүй: амжилтгүй бол session failed, логт eidsign: eID reported a signature that does not verify (reason-той, PII-гүй). EID_SIGN_MOCK_MODE-д баталгаажуулалт ажиллахгүй.

PDF-ийн урсгал (client-side PAdES). eID 2026-10 сард /signature/stamp-ийг хассан (бүх хост дээр 404). PDF eID-д очихгүй: eidsign нь eID-ийн SDK (eid-mongolia-sdk-go/pades)-аар баримтыг өөрөө угсарна — (1) иргэний утсанд гэрчилгээ сонгуулах хүсэлт (certificate choice, PIN-гүй, нэг товшилт), (2) тэр гэрчилгээгээр pades.Prepare, DTBS-ийг ЯГ тэр төхөөрөмж рүү PIN2-оор (/signature/notification/linked/{documentNumber}), (3) гарын үсгийг дээрх шалгалтаар (digest = DTBS, гэрчилгээ = сонгосон гэрчилгээ), (4) татахад pades.Complete (TSA + OCSP/CRL → PAdES-BASELINE-LT) ба eID-д бүртгэл (POST /pdf/signatures, /verify хуудас). Дуудагчид нэг session: хоёр алхмын турш running. eID state-ийн хугацаа eID-ийн session-ий expiresAt-аас тоологдоно. Document-Signer-ийн орлуулга (EID_SIGN_SIGNER_CERT_PEM/_KEY_PEM) хасагдсан — тохируулсан утга нөлөөгүй.

Хэмжээ. eID-ийн PAdES профайл 10 223 616 байтаас (pades.MaxInputBytes) их PDF зурдаггүй тул eID-ээр зурах PDF ~9.75 MiB хүртэл (тавиур дээрх баримт 25 MB хүртэл байж болно — түүнийг eID-ээр зурахад 413 PAYLOAD_TOO_LARGE).

Complete унавал. /esign модуль баталгаажсан гарын үсгийг эхлээд stamping төлөвт бичнэ (migration 00136): eID state (бэлтгэсэн PDF, гарын үсэг, гэрчилгээ) 25 цаг, session 24 цаг (eidsign.StampWindow) хадгалагдаж, хүлээлтийн sweep (70 мин) түүнийг abandoned болгохгүй. TSA эсвэл OCSP/CRL унавал (TSA_UNAVAILABLE, LTV_UNAVAILABLE) ErrStampUnavailable — PIN2 дахин асуухгүйгээр ижил гарын үсгийг дахин complete хийнэ. eID-ийн бүртгэл амжилтгүй бол PDF өгөгдөж, дараагийн татахад дахин бүртгэнэ (ERROR eidsign: eID did not register the signed document); оролдлого бүрийг мөр дээр claimStampAttempt (stamp_next_at <= NOW() нөхцөлтэй UPDATE … RETURNING) эзэмшдэг тул олон poll / олон replica зэрэг 10 MiB-ийг давтан илгээхгүй; backoff 30с → 1м → 2м → 4м → 5м (дээд). Frontend энэ үед «Гарын үсэг баталгаажлаа — eID-ийн тамгыг хүлээж байна» гэж харуулж 5 секунд тутам poll хийнэ. Лог: ERROR esign: the signed document cannot be completed yet; the verified signature is kept. 24 цагт stamp ирээгүй бол sweep failed / stamp_unavailable болгож ERROR esign: signed ceremonies closed because eID never stamped them бичнэ. Шалгах: SELECT state, stamp_attempts, stamp_next_at, expires_at FROM workspace.esign_sign_sessions WHERE state = 'stamping';. Удаан үргэлжилбэл tsa.timeserver.mn ба гэрчилгээний OCSP/CRL-ийг шалга. Retry-г poll хийгээд зогсохгүй: иргэн хуудсаа хаасан ч housekeeping 5 минут тутам хугацаа нь болсон stamping мөрүүдийг (нэг удаад 10) ижил claim-ээр оролдоно. eID state алга болсон (ErrStampUnavailable биш) бол дахин оролдохгүй — failed / stamp_state_lost. eID state-д completed бичиж чадаагүй poll алдаа буцааж session pending үлдэнэ. nexus.Signer.SignedDocument ижил Stamp-ийг дуудна.

Trust store. Production-д EID_RP_UUID тохируулсан үед eID-ийн trust store (embedded гинж) угсрагдахгүй бол API эхлэхгүй (config.ValidateProduction → validateEIDTrustStore; eidmongolia.New → requireSignTrust). Өмнө нь нэвтрэлт чимээгүй off болж, гарын үсэг бүр PIN2-ын дараа унадаг байв. Бусад орчинд лог л бичнэ.

API-ийн өгөгдлийн сангийн role

API нь анхдагчаар postgres superuser-ээр холбогддог. dbguard тенант ба консолын хүсэлтэд эрхгүй role руу SET ROLE хийдэг ч session-гүй зам (нэвтрэх, eID/DAN, /oauth2/*, setup, sweep) login role дээрээ ажилладаг — тэр нь superuser бол процесс дотор код ажиллуулсан хэн ч COPY … TO PROGRAM, ALTER TABLE … DISABLE ROW LEVEL SECURITY, CREATE ROLE хийж чадна.

Migration 00115 нь gerege_nexus_api role үүсгэнэ: LOGIN NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOINHERIT, юу ч эзэмшихгүй, схемд CREATE эрхгүй, tenant/operator role-ийн гишүүн, бүх хүснэгтэд DML (audit хүснэгтэд UPDATE/DELETE-гүй), RLS-тэй хүснэгт бүр дээр өөрийн platform_path бодлоготой. Нууц үг migration-д байхгүй тул role үүссэн ч шилжүүлэх хүртэл юу ч өөрчлөгдөхгүй.

API эхлэхэд dbguard login role-ыг шалгана. Superuser, BYPASSRLS, эсвэл тийм role болж чадах (SET ROLE) бол production-д эхлэл бүрт ERROR dbguard: the API connects to the database as a privileged role бичиж, db_login_role_privileged хэмжүүрийг 1 болгоно (Prometheus-д db_login_role_privileged == 1 сэрэмжлүүлэг тавих боломжтой). Эхлэлийг зогсоохгүй — шилжээгүй суулгац ажилласаар байна.

Шилжүүлэх дараалал

  1. 00115 хэрэгжсэн deploy гарсан байх (migrate контейнер postgres-ээр ажилласаар байна — migration нь эзэмшигч role шаарддаг).
  2. Нууц үг үүсгээд role-д өгнө (утгыг shell түүхэнд үлдээхгүйн тулд \password):
docker exec -it <угтвар>_postgres psql -U postgres -d platform_db
platform_db=# \password gerege_nexus_api
  1. Шалгана:
SELECT rolname, rolsuper, rolbypassrls, rolcreaterole, rolcreatedb, rolinherit, rolcanlogin
  FROM pg_roles WHERE rolname = 'gerege_nexus_api';
-- f, f, f, f, f, t  байх ёстой
SELECT pg_has_role('gerege_nexus_api', 'gerege_nexus_tenant', 'MEMBER'),
       pg_has_role('gerege_nexus_api', 'gerege_nexus_operator', 'MEMBER');
-- t, t
SELECT count(*) FROM pg_class c WHERE c.relrowsecurity AND NOT EXISTS (
  SELECT 1 FROM pg_policy p WHERE p.polrelid = c.oid AND p.polname = 'platform_path');
-- 0 (0 биш бол тэр хүснэгтийг session-гүй зам харахгүй)
  1. Суулгацын .env-д нэмнэ (API_DATABASE_URL нэр нь жишээ):
API_DATABASE_URL=postgres://gerege_nexus_api:<нууц үг>@postgres:5432/platform_db?sslmode=disable

Compose файлын backend үйлчилгээний DATABASE_URL postgres-ээр хатуу бичигдсэн бол тэр мөрийг DATABASE_URL: ${API_DATABASE_URL:-postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/platform_db?sslmode=disable} болгоно. migrate үйлчилгээнийх postgres хэвээр үлдэнэ. Нууц үгэнд URL-д тусгай утгатай тэмдэгт (@ : / ? #) байвал percent-encode хийнэ. 5. Deploy хийсний дараа логт dbguard: the database login role is unprivileged гарч, db_login_role_privileged 0 болсныг шалгана. Дараа нь хөндөгдсөн бүх session-гүй замыг гараар туршина: нууц үгээр нэвтрэх, eID/DAN нэвтрэлт, Google нэвтрэлт, /oauth2/authorize → /oauth2/token, консолын нэвтрэлт (TOTP), имэйл баталгаажуулах холбоос, апп суулгах, мөн sweep-ийн логт permission denied байхгүй эсэх (docker logs <угтвар>_backend 2>&1 | grep -i "permission denied\|42501").

Буцаах: API_DATABASE_URL-ыг .env-ээс хасаад backend-ыг дахин асаа — compose-ийн анхдагч утга postgres руу буцна.

Цаашдын migration ба модуль

  • RLS идэвхжүүлсэн шинэ хүснэгт бүр platform_path бодлогоо нэмнэ: CREATE POLICY platform_path ON <schema>.<table> TO gerege_nexus_api USING (true) WITH CHECK (true); Мартвал db/migrations/api_role_test.go CI дээр унана. Grant-ыг ALTER DEFAULT PRIVILEGES шийднэ (migration postgres-ээр ажиллах үед). Модулийн tenant_id-тай хүснэгтэд энэ бодлого дутуу бол консолын байгууллагын экспорт (GET /api/platform/v1/tenants/{id}/export) тэр хүснэгтийг нэрлэж унана — хоосон мэт алгасахгүй.
  • Модулийн өөрийн migration (nexus.Migrations) API ажиллах үед API-ийн pool-оор хэрэгждэг. gerege_nexus_api DDL хийж чадахгүй тул тэнд хэрэгжээгүй модулийн migration «permission denied for schema» гэж лог бичнэ — тиймээс модулийн schema-г migrate алхам эзэмшигч role-оор хэрэгжүүлдэг: цөмд багтсан модуль (internal/apps.Migrations, одоогоор API гарц)-ийг cmd/migrate up, distribution-ий өөрийн модулийг тухайн distribution-ий cmd/migrate (migrate.Main(migrate.Options{Modules: map[string]fs.FS{<ID>: <FS>}}), backend/pkg/migrate). Түлхүүр нь модулийн бүртгэлийн ID — public.goose_db_version_<ID-ийн сүүлийн хэсэг> хүснэгт API-ийнхтай ижил тул хэрэгжсэн хувилбар дахин ажиллахгүй, API-ийн дараагийн sweep юу ч хийхгүй. Зөвхөн платформын өөрийн түүхээр (MIGRATIONS_TABLE тавиагүй) up хийхэд. Модулийн шинэ хүснэгт RLS-тэй бол platform_path бодлогыг мөн нэмнэ. Deploy-ийн down-to буцаалт тэдгээрийг хөндөхгүй.

Цөмийн шинэчлэл (/cp/ops/updates)

Дистрибуц (жишээ нь petronet-gerege-nexus) энэ репог core/ болгон авч явдаг, core/SNAPSHOT нь аль commit болохыг тэмдэглэдэг, тэр репогийн .github/workflows/core-sync.yml нь цөм шинэчлэгдэх бүрд core-sync/<sha12> салбараас core: nexus-gerege-private@<sha8> PR нээдэг. Консолын «Шинэчлэл» дэлгэц тэр PR-ийг (ямар commit авчирч байгаа, CI ногоон эсэх) харуулж, оператор шийдвэл workflow-оор merge хийлгэнэ. Консол өөрөө merge хийхгүй: бичих токен workflow-д, салбарын хамгаалалт хэвээр.

Env (backend; хоёр эхнийх нь хоосон бол дэлгэц «тохиргоогүй» гэж дутуу нэрийг харуулна, GitHub руу юу ч асуухгүй):

Хувьсагч Утга
CORE_SYNC_REPOSITORY Дистрибуцын репо, owner/name.
GITHUB_SYNC_TOKEN Fine-grained PAT, зөвхөн тэр репо: Actions — Read and write, Pull requests — Read, Commit statuses — Read, Checks — Read (сонголтын жагсаалтад байвал), Contents — Read; токен check run уншиж чадахгүй бол дэлгэц unauthorized гэнэ. Merge эрх өгөхгүй — merge-ийг workflow өөрийн токеноор хийнэ.
CORE_SYNC_WORKFLOW Анхдагч core-sync.yml.
CORE_SNAPSHOT_FILE Анхдагч /app/CORE_SNAPSHOT — image-д хуулсан core/SNAPSHOT.

API (консолын /api/platform/v1):

  • GET /updates (tenant.read) — {configured, missing[], repository, running: {core_sha, core_date, platform, release}, pending: null | {number, url, title, branch, head_sha, target_sha, created_at, ci: success|pending|failure, mergeable_state, commits: [{sha, subject}]}, github_error?: unauthorized|not_found|unavailable, checked_at}. GitHub-ийн хариуг процесс дотор 60 секунд хадгална.
  • POST /updates/apply {pr_number} (deploy.trigger, аудит core.sync.apply) — PR-ийг GitHub-аас дахин уншиж, одоогийн core-sync PR биш бол 409, CI ногоон биш бол 409. Тэгээд CORE_SYNC_WORKFLOW-ийг main дээр inputs: {merge_pr: "<дугаар>"}-тэй dispatch хийж, 202 {status: "started", url: <Actions хуудас>} буцаана.

pending нь нээлттэй PR-уудаас head салбар нь core-sync/-ээр эхэлсэн, тэр репогийн өөрийнх (fork биш) хамгийн шинэ нь. CI: head commit-ийн combined status ба check run-ууд — аль нэг нь унасан бол failure, дуусаагүй эсвэл юу ч мэдээлээгүй бол pending, бусад үед success.

Дистрибуцын хийх гурван зүйл:

  1. Snapshot-ийг image-д хуулах CORE_SNAPSHOT_FILE руу (жишээ нь COPY core/SNAPSHOT /app/CORE_SNAPSHOT). Формат: мөр бүр түлхүүр: утга; ref: <sha> заавал, date: <RFC 3339> (цөмийн commit-ийн огноо) сонголттой. Файлгүй бол дэлгэц «Snapshot файл алга» гэнэ, бусад нь ажиллана.
  2. core-sync.yml нь workflow_dispatch-д merge_pr input хүлээн авч, тэр PR өөрийн core-sync/ салбар мөн эсэхийг шалгаад merge хийнэ. PR-ийн body-д авчирч буй commit-уудыг дараах блокоор бичнэ (мөр бүр <hash> <subject>, hash 7–40 hex; блокоос гадуурх бүхэн үл тоомсорлогдоно; эхний 200 мөр л уншигдана):
```core-commits
1a2b3c4d fix(eid): bind the signed-in identity to the certificate (#76)
5e6f7a8b feat(console): core updates screen
```
  1. Консолоор merge хийхээр шилжихдээ gh pr merge … --auto-г хасна — эс бөгөөс CI ногоон болмогц GitHub өөрөө merge хийж, дэлгэцэд шийдэх зүйл үлдэхгүй.

GitLab дээрх дистрибуц (CORE_SYNC_PROVIDER=gitlab)

Гэрээ ижил, GitLab-ийн үгээр: PR-ийн оронд MR, CI-ийн оронд MR-ийн head pipeline, workflow dispatch-ийн оронд pipeline trigger. JSON хариу ижил (provider: "gitlab" нэмэгдэнэ; number = MR-ийн iid; github_error нэр нь хуучин дэлгэцүүдийн төлөө хэвээр, GitLab-ийн алдааг ч мөн тэнд бичнэ). Код: backend/internal/operator/backup/updates_gitlab.go.

Хувьсагч Утга
CORE_SYNC_PROVIDER gitlab (анхдагч github; өөр утга → «дутуу: CORE_SYNC_PROVIDER»).
CORE_SYNC_GITLAB_URL GitLab-ийн хаяг, жишээ нь https://gitlab.example.mn (төгсгөлийн /-гүй ч, тэй ч болно).
CORE_SYNC_REPOSITORY Төслийн зам (group/sub/project, API дуудлагад %2F-ээр escape хийгдэнэ) эсвэл тоон id.
CORE_SYNC_GITLAB_TOKEN Уншихад л: Project access token, role Reporter, scope read_api. MR, pipeline уншина; merge, push эрхгүй. PRIVATE-TOKEN header-ээр явна.
CORE_SYNC_TRIGGER_TOKEN Зөвхөн «Шинэчлэх» товчинд: Settings → CI/CD → Pipeline trigger tokens. Унших токентой хэзээ ч холилдохгүй.

Дөрөв нь бүгд байхгүй бол дэлгэц «тохиргоогүй» гэж дутууг нэрлэнэ, GitLab руу юу ч асуухгүй. CORE_SYNC_WORKFLOW GitLab-д хэрэглэгдэхгүй.

  • pending: GET /projects/:id → default_branch; GET /projects/:id/merge_requests?state=opened&target_branch=<default> — source_branch нь core-sync/-ээр эхэлсэн, source_project_id == target_project_id (fork биш) хамгийн шинэ нь; дараа нь GET /projects/:id/merge_requests/:iid (head_pipeline, detailed_merge_status, description).
  • CI: head_pipeline.status — success → success; failed, canceled → failure; бусад бүх төлөв ба pipeline огт үгүй → pending.
  • mergeable_state = detailed_merge_status (mergeable, ci_still_running, conflict…), зөвхөн харуулна.
  • POST /updates/apply {reason, pr_number: <iid>} — MR-ийг дахин уншиж (409: одоогийнх биш / pipeline ногоон биш), аудит core.sync.apply, тэгээд:
curl -X POST "$CORE_SYNC_GITLAB_URL/api/v4/projects/<id|escaped path>/trigger/pipeline" \
  --form "token=$CORE_SYNC_TRIGGER_TOKEN" \
  --form "ref=<MR-ийн target_branch = default branch>" \
  --form "variables[MERGE_MR]=<iid>"

201-ийн web_url-ийг (тэр GitLab-ийн хаяг мөн бол, эс бөгөөс MR-ийн хуудсыг) 202 {status: "started", url}-ээр буцаана. 201-ээс өөр бол 400 «GitLab refused to start the pipeline», GitLab-ийн хариу биеийг хэзээ ч дамжуулахгүй, статус нь лог-д. Уншихад 401/403 → unauthorized, 404 → not_found, бусад → unavailable.

Дистрибуцын .gitlab-ci.yml хийх зүйл: core-sync/<sha12> салбараас default branch руу MR нээж, description-д дээрх ```core-commits блокыг бичих; MERGE_MR хувьсагчтай trigger pipeline дээр ($CI_PIPELINE_SOURCE == "trigger") core-merge job ажиллаж, тэр iid өөрийн core-sync/ салбар, pipeline ногоон эсэхийг өөрөө шалгаад merge хийх; MR-т «auto-merge»/«merge when pipeline succeeds» тавихгүй (GitHub-ийн --auto-той ижил шалтгаанаар).

Production орчны асахгүй тохиргоонууд

ENVIRONMENT=production тавьсан ямар ч суулгац (staging ч гэсэн) дараахтай асахгүй:

  • EID_MOCK_MODE эсвэл ESIGN_MOCK_MODE = true/1/yes — дурын регистрийг баталгаажсан иргэн болгож нэвтрүүлдэг (e-sign-ийнх нь гарын үсгийг хуурамчаар «баталгаажуулна»). Staging-д mock хэрэгтэй бол ENVIRONMENT-ийг production биш утгаар тавь.
  • EID_VERIFY_RESPONSE нь enforce биш (дээрх eID хэсгийг үз).

ENVIRONMENT огт тавиагүй ч PUBLIC_ORIGIN нь https:// бол суулгац мөн асахгүй — HTTPS хаягтай сервер дээр хөгжүүлэлтийн анхдагч утгууд санамсаргүй үйлчлэхээс сэргийлнэ. Mock (*_MOCK_MODE) болон SEED_DEMO_DATA нь ямар ч орчинд зөвхөн explicit true үед асна.

INTEGRATION_ENCRYPTION_KEY хэзээ ч солигдохгүй: операторын TOTP нууц ба OIDC гарын үсгийн түлхүүр түүгээр битүүмжлэгддэг. Солих шаардлагатай бол: эхлээд доорх oidc-signing-key -rotate-ийг шинэ түлхүүртэй ажиллуулж, дараа нь операторуудыг operator-bootstrap-ээр дахин бүртгэнэ (TOTP хэсгийг үз).

OAuth2: introspection ба refresh token

  • /oauth2/introspect нь active:true-г зөвхөн токен олгогдсон клиентэд эсвэл тухайн байгууллагад бүртгэлтэй confidential клиентэд өгнө. Гадны resource server токен эзэмшигчийн байгууллагад confidential клиентээр бүртгэгдэнэ.
  • Refresh token ашиглах бүрд эргэнэ. Нэг токеныг зэрэг хоёр удаа илгээвэл (хоёр tab) replay гэж тооцогдож хэрэглэгч гарна — клиент refresh-ийг single-flight хийнэ. 5xx хариу хуучин токеныг хүчинтэй үлдээдэг тул дахин оролдох аюулгүй.

CONCURRENTLY migration-ууд (00119, 00120)

-- +goose NO TRANSACTION файлууд индексийг бичилт хаалгүй үүсгэнэ. Тасалдвал INVALID индекс үлдэж, IF NOT EXISTS түүнийг алгасна. Шалгах: SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid; — байвал DROP INDEX CONCURRENTLY <нэр>; хийгээд migrate-ийг дахин ажиллуул. 00120 нь pg_trgm extension үүсгэнэ (migrate postgres-ээр ажилладаг тул болно).

gerege_nexus_api нь goose-ийн хувилбарын хүснэгтэд (goose_db_version, goose_db_version_<slug>) бичих эрхгүй. 00115-ийн ALTER DEFAULT PRIVILEGES дараа үүссэн хүснэгтэд эрхийг дахин олгодог тул cmd/migrate ажил бүрийн төгсгөлд эзэмшигчээр REVOKE-г давтана; api_role_test.go эрх үлдсэн бол унана.

OIDC гарын үсгийн түлхүүр

id_token-д гарын үсэг зурдаг RSA түлхүүр registry.oauth2_signing_keys-д INTEGRATION_ENCRYPTION_KEY-ээр битүүмжлэгдэж хадгалагдана (migration 00116, sealed:v1: угтвартай). Хуучин энгийн PEM мөрүүдийг API эхлэхдээ битүүмжилнэ (sealed legacy OIDC signing keys лог).

  • INTEGRATION_ENCRYPTION_KEY production-д заавал. Байхгүй бол одоо байгаа түлхүүр ажилласаар (ERROR лог), харин шинэ түлхүүр үүсгэх/эргүүлэх татгалзана — хоосон DB дээр SSO нэвтрэлт id_token-гүй болно.
  • Түлхүүрийг солихгүй. Битүүмжилсэн түлхүүрийг өөр INTEGRATION_ENCRYPTION_KEY-ээр нээж чадахгүй: API id_token гаргахаа болино. Солих шаардлагатай бол эхлээд доорх -rotate-ийг шинэ түлхүүртэй ажиллуул.
  • 00116-г буцаавал (goose down) мөрүүд битүүмжлэгдсэн хэвээр үлдэх тул хуучин бинарь id_token зурж чадахгүй — кодыг буцаахаас өмнө түлхүүрийг эргүүлэх биш, энэ migration-ийг буцаахгүй байх нь зөв.

Эргүүлэх (backend/cmd/oidc-signing-key, API-тай ижил DATABASE_URL ба INTEGRATION_ENCRYPTION_KEY-ээр):

cd backend
go run ./cmd/oidc-signing-key            # жагсаалт, хувийн хэсэггүй
go run ./cmd/oidc-signing-key -rotate    # шинэ идэвхтэй түлхүүр
go run ./cmd/oidc-signing-key -seal      # энгийн PEM үлдсэн бол

Эргүүлсний дараа replica бүр 5 минутын дотор шинэ түлхүүрээр зурна; хуучин түлхүүр JWKS-д 1ц15м (access token-ы TTL + кэш + JWKS max-age) нийтлэгдсээр байна. Relying party-ууд JWKS-ээ үл мэдэгдэх kid дээр дахин татах ёстой. Бинарь одоогоор Docker image-д ороогүй (deploy/Dockerfile).