Cloudflare Workers ์์์ ๋์ํ๋ ์คํ์์ค Identity Provider์ ๋๋ค. OIDC, SAML 2.0, WebAuthn/Passkey, TOTP 2FA, LDAP ์ฐ๋์ ์ง์ํ๋ฉฐ ๋ฉํฐํ ๋ํธ ์กฐ์ง ๊ด๋ฆฌ๋ฅผ ํฌํจํฉ๋๋ค.
๊ฐ๋ฐ ๋จ๊ณ ์๋ด: ๋ณธ ํ๋ก์ ํธ๋ ํ๋ฐํ ๊ฐ๋ฐ ์ค์ธ ํ์ต/์คํ ์ฑ๊ฒฉ์ IdP์ ๋๋ค. ํ๋ก๋์ ๋์ ์ ์๋ ์ํ ๋ชจ๋ธ์ ๋ง์ถ ์์ฒด ๋ณด์ ๊ฒํ ๋ฅผ ๊ถ์ฅํฉ๋๋ค. ๋ณด์ ๊ด๋ จ ์๋ ค์ง ํ๊ณ๋ ๋ณด์ ์ฐธ๊ณ ์ฌํญ์ ์ฐธ์กฐํ์ธ์.
- ์ฃผ์ ๊ธฐ๋ฅ
- ๊ธฐ์ ์คํ
- ๋๋ ํฐ๋ฆฌ ๊ตฌ์กฐ
- ์๋ํฌ์ธํธ ๊ฐ์
- ์์ํ๊ธฐ
- ํ๊ฒฝ๋ณ์
- ๊ฐ๋ฐ ์ํฌํ๋ก
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
- ๋ณด์ ์ฐธ๊ณ ์ฌํญ
- ๋ผ์ด์ ์ค
| ๊ธฐ๋ฅ | ์ค๋ช |
|---|---|
| OIDC | Authorization Code + PKCE, Refresh Token, UserInfo, JWKS, End-Session |
| SAML 2.0 | SP-Initiated SSO, HTTP-POST ๋ฐ์ธ๋ฉ, ForceAuthn, IsPassive, RequestedAuthnContext, SLO |
| ACR / AMR | ์ธ์ฆ ๋ฐฉ์์ ๋ฐ๋ฅธ ACR ์๋ ๊ฒฐ์ โ SAML Assertion ๋ฐ OIDC ID Token์ ํฌํจ |
| WebAuthn / Passkey | ํจ์คํค ๋ฑ๋ก ๋ฐ ์ธ์ฆ, challenge 1ํ์ฉ DB ์ฒ๋ฆฌ, ํ ๋ํธ ๊ฒฉ๋ฆฌ |
| TOTP 2FA | Google Authenticator ๋ฑ ํธํ, ๋ฐฑ์ ์ฝ๋ ์ง์ |
| LDAP ์ฐ๋ | LDAP ์ธ์ฆ ๋ฐ JIT ์ฌ์ฉ์ ํ๋ก๋น์ ๋, ๊ด๋ฆฌ์ UI์์ ํ๋ก๋ฐ์ด๋ ์ค์ |
| ๊ณ์ ์๊ฐ ๊ด๋ฆฌ | ํ๋กํ ํธ์ง, ๋น๋ฐ๋ฒํธ ์ฌ์ค์ /์ฐพ๊ธฐ, MFA ๋ฑ๋ก, Passkey ๋ฑ๋กยทํด์ |
| ์กฐ์ง ๊ด๋ฆฌ | ๋ถ์ โ ํ โ ํํธ ๊ณ์ธต, ์ง๊ธ/์ง์ฑ , ๋ณต์ ์์ |
| ๋ฉํฐํ ๋ํธ | ํ ๋ํธ๋ณ ๋ ๋ฆฝ ์ฌ์ฉ์/ํด๋ผ์ด์ธํธ/ํค ๊ด๋ฆฌ |
| ๊ด๋ฆฌ์ UI | ์ฌ์ฉ์, ์กฐ์ง(๋ถ์ยทํยทํํธยท์ง๊ธ), OIDC ํด๋ผ์ด์ธํธ, SAML SP, LDAP ํ๋ก๋ฐ์ด๋, ์๋ช ํค, ๋ก๊ทธ์ธ ์คํจ, ๊ฐ์ฌ ๋ก๊ทธ CRUD |
| ์ปค์คํ ๋ก๊ทธ์ธ ์คํจ | OIDC ํด๋ผ์ด์ธํธ๋ณ ์ปค์คํ CSS/์คํฌ๋ฆฝํธ, R2 ์บ์๋ก ๋ฐฐํฌ |
| ๊ฐ์ฌ ๋ก๊ทธ | ๋ก๊ทธ์ธ, SSO, ํ ํฐ ๋ฐ๊ธ ๋ฑ ์ฃผ์ ์ด๋ฒคํธ ์๋ ๊ธฐ๋ก, ๊ด๋ฆฌ์ UI์์ ์กฐํ ๊ฐ๋ฅ |
| ๊ตญ์ ํ | ๋ฉ์์ง ์นดํ๋ก๊ทธ ๊ธฐ๋ฐ i18n (ํ์ฌ ํ๊ตญ์ด ์ ๊ณต, ๋ค๊ตญ์ด ํ์ฅ ๊ฐ๋ฅ) |
์ธ์ ์ ์ธ์ฆ ๋ฐฉ์(AMR)์ ๋ฐ๋ผ ACR์ด ์๋์ผ๋ก ๊ฒฐ์ ๋์ด SAML Assertion ๋ฐ OIDC ID Token์ ํฌํจ๋ฉ๋๋ค.
| ์ธ์ฆ ๋ฐฉ์ | AMR ๊ฐ | ACR ๊ฐ |
|---|---|---|
| ๋น๋ฐ๋ฒํธ๋ง | pwd |
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport |
| ๋น๋ฐ๋ฒํธ + TOTP | pwd, totp |
https://refeds.org/profile/mfa |
| ๋น๋ฐ๋ฒํธ + ๋ฐฑ์ ์ฝ๋ | pwd, swk |
https://refeds.org/profile/mfa |
| WebAuthn / Passkey | hwk |
https://refeds.org/profile/mfa |
SAML SP๊ฐ RequestedAuthnContext๋ก ํน์ ACR์ ์๊ตฌํ๋ ๊ฒฝ์ฐ, ์ธ์
ACR์ด ํด๋น ์์ค์ ๋ง์กฑํ์ง ์์ผ๋ฉด ์ฌ์ธ์ฆ์ด ๊ฐ์ ๋ฉ๋๋ค. MFA ๋ฏธ์ค์ ๋ฑ์ผ๋ก ๋ง์กฑ ๋ถ๊ฐ ์ NoAuthnContext ์ค๋ฅ๊ฐ ACS URL๋ก ๋ฐํ๋ฉ๋๋ค.
๊ฐ SP์ allowedAttributes ๋ชฉ๋ก์ ์ค์ ํ์ฌ Assertion์ ํฌํจํ ์์ฑ์ ์ ์ดํ ์ ์์ต๋๋ค. ๋ฏธ์ค์ ์ email, username, displayName๋ง ๊ธฐ๋ณธ ํฌํจ๋ฉ๋๋ค. ์กฐ์ง ์ ๋ณด(department, team, jobTitle, position)๋ SP๊ฐ ๋ช
์์ ์ผ๋ก ํ์ฉํ ๊ฒฝ์ฐ์๋ง ํฌํจ๋ฉ๋๋ค.
- Runtime: Cloudflare Workers(๊ธฐ๋ณธ,
nodejs_alsยทnodejs_compat) ๋๋ ์์ Node ์๋ฒ(adapter-node,BUILD_TARGET=node) โ ๋ฐฐํฌ ํ๊น ์ฐธ๊ณ - Framework: SvelteKit 2 + Svelte 5 (runes),
@sveltejs/adapter-cloudflare - Database: Cloudflare D1(SQLite) / libSQL(SQLite) / PostgreSQL / MySQL ์ค ํ1 (Drizzle ORM). PostgreSQLยทMySQL ์ Hyperdrive ๋๋
DATABASE_URL์ง๊ฒฐ (DB ๋ฐฉ์ธ ์ ํ ์ฐธ๊ณ ) - Object Storage: Cloudflare R2 ๋๋ S3 ํธํ(AWS S3ยทMinIO ๋ฑ) โ ์ปค์คํ ๋ก๊ทธ์ธ ์คํจ ์บ์
- Styling: Tailwind CSS 4
- Crypto: Web Crypto API (RSA/EC ์๋ช
, argon2id ๋น๋ฐ๋ฒํธ ํด์),
@simplewebauthn/*,xmldsigjs - Language / Tooling: TypeScript, Bun, ESLint, Prettier
src/
โโโ hooks.server.ts # ์ธ์
๋ณต์, ๋ณด์ ํค๋, ํ
๋ํธ ์ปจํ
์คํธ
โโโ app.html
โโโ routes/
โ โโโ (auth)/ # login, signup, logout, mfa, find-id, find-password, reset-password
โ โโโ account/ # profile, mfa, passkeys (๊ณ์ ์๊ฐ ๊ด๋ฆฌ)
โ โโโ admin/ # ๊ด๋ฆฌ์ UI (users, departments, teams, parts, positions,
โ โ # oidc-clients, saml-sps, ldap-providers,
โ โ # signing-keys, skins, audit, login)
โ โโโ oidc/ # authorize, token, userinfo, jwks, end-session
โ โโโ saml/ # sso, slo, metadata
โ โโโ api/ # webauthn/*, skin-scripts/*
โโโ lib/
โโโ i18n/ # ๋ฉ์์ง ์นดํ๋ก๊ทธ (ko.json)
โโโ assets/
โโโ server/
โโโ auth/ # session, password, mfa, totp, webauthn, guards, bootstrap
โโโ oidc/ # client, grant, pkce, logout
โโโ saml/ # sp, metadata, parse-authn-request, response, slo
โโโ ldap/ # auth, client, provision
โโโ crypto/ # ์๋ช
ํค ๊ด๋ฆฌ, JWT ๋ฐ๊ธ, ํค ํ์
โโโ audit/ # ๊ฐ์ฌ ์ด๋ฒคํธ ๊ธฐ๋ก
โโโ org/ # ์กฐ์ง ๋ฉค๋ฒ์ญ ์กฐํ
โโโ ratelimit/ # ์ธ์ฆ ์๋ํฌ์ธํธ ๋ ์ดํธ ๋ฆฌ๋ฐ
โโโ skin/ # ์ปค์คํ
๋ก๊ทธ์ธ ์คํจ
โโโ db/ # Drizzle ์คํค๋ง ๋ฐ D1 ์ด๊ธฐํ
drizzle/ # ๋ง์ด๊ทธ๋ ์ด์
SQL (drizzle-kit generate ์ฐ์ถ๋ฌผ)
docs/ # ์ค๊ณ/๊ฐ์ฌ ๋ฌธ์
scripts/setup.ts # ๋ํํ ์ด๊ธฐ ์
์
์คํฌ๋ฆฝํธ
| ๊ฒฝ๋ก | ์ค๋ช |
|---|---|
/.well-known/openid-configuration |
OIDC Discovery ๋ฌธ์ |
/oidc/authorize |
์ธ์ฆ ์์ฒญ (Authorization Code + PKCE) |
/oidc/token |
ํ ํฐ ๊ตํ (Authorization Code, Refresh Token) |
/oidc/userinfo |
UserInfo ์๋ํฌ์ธํธ |
/oidc/jwks |
JSON Web Key Set |
/oidc/end-session |
RP-Initiated Logout |
| ๊ฒฝ๋ก | ์ค๋ช |
|---|---|
/saml/metadata |
IdP ๋ฉํ๋ฐ์ดํฐ XML |
/saml/sso |
SP-Initiated SSO (AuthnRequest ์ฒ๋ฆฌ) |
/saml/slo |
Single Logout (์ฒด์ธ SLO ์ง์) |
| ๊ฒฝ๋ก | ์ค๋ช |
|---|---|
/api/webauthn/register/options |
Passkey ๋ฑ๋ก challenge ๋ฐ๊ธ |
/api/webauthn/register/verify |
Passkey ๋ฑ๋ก attestation ๊ฒ์ฆ |
/api/webauthn/authenticate/options |
Passkey ์ธ์ฆ challenge ๋ฐ๊ธ |
/api/webauthn/authenticate/verify |
Passkey ์ธ์ฆ assertion ๊ฒ์ฆ |
/api/skin-scripts/* |
OIDC ํด๋ผ์ด์ธํธ๋ณ ์ปค์คํ ์คํจ ์คํฌ๋ฆฝํธ |
stardust dispatcher ๊ฐ์ ์ ๋ขฐ๋ ์๋น์ค๊ฐ ์ฌ์ฉ์ ๋์ TOTP ๋ฑ๋ก / ๊ฒ์ฆ์ ์ํํ๊ธฐ ์ํ API. ๋ชจ๋ ์์ฒญ์ Authorization: Bearer $DISPATCHER_SERVICE_TOKEN ํค๋ ํ์ (constant-time ๋น๊ต).
| ๊ฒฝ๋ก | ๋ฉ์๋ | ์ค๋ช |
|---|---|---|
/api/totp/enroll/init |
POST | {userId} โ {secret, otpAuthUri, username} (stateless) |
/api/totp/enroll/confirm |
POST | {userId, secret, code} โ ๊ฒ์ฆ + ์๊ตฌ ์ ์ฅ + {backupCodes} |
/api/totp/verify |
POST | {userId, code} โ step-up ๊ฒ์ฆ + {ok, verifiedAt} |
/api/totp/status |
GET | ?userId=... โ {enrolled, backupCodeCount, lastUsedAt} |
- Bun 1.x
- Cloudflare ๊ณ์ (D1, R2, Workers ํ์ฑํ)
- Wrangler CLI (
bun add -g wrangler)
git clone https://github.com/mack-erel/idp.git
cd idp
bun install
bun run setupbun run setup์ ์๋ ๊ณผ์ ์ ๋ํํ์ผ๋ก ์๋ดํฉ๋๋ค. DB ๋ฐฉ์ธ์ --dialect(d1(๊ธฐ๋ณธ) | postgres | mysql | sqlite) ๋๋ DB_DIALECT ํ๊ฒฝ๋ณ์๋ก ์ ํํ๋ฉฐ, ๋ฐฉ์ธ์ ๋ฐ๋ผ 3~5๋จ๊ณ๊ฐ ๊ฐ๋ผ์ง๋๋ค:
- ์ค์ ํ์ผ ์์ฑ โ
wrangler.example.jsoncโwrangler.jsonc,.env.exampleโ.env - DB ๋ฐฉ์ธ ๊ฒฐ์ โ
--dialect>DB_DIALECTenv >.env> ํ๋กฌํํธ - DB ์ค์
d1: wrangler ๋ก๊ทธ์ธ ํ์ธ ํ D1 ์๋ก ์์ฑ ๋๋ ๊ธฐ์กด DB ์ ํ (ํ๋ฆฌ๋ทฐ DB ์ ํ์ ), DB IDยท๊ณ์ ID๋ฅผwrangler.jsonc/.env์ ์๋ ๊ธฐ์postgres/mysql/sqlite:DATABASE_URL์ ๋ ฅ(๋๋ ๊ธฐ์กด ๊ฐ ์ฌ์ฌ์ฉ), postgres/mysql ์ Hyperdrive ๊ตฌ์ฑ ID ์ ๋ ฅ ์wrangler.jsonc์HYPERDRIVE๋ฐ์ธ๋ฉ ์๋ ์ค์
- ๋ง์ด๊ทธ๋ ์ด์
โ ๋ฐฉ์ธ๋ณ
db:generate*์คํ ํ ์คํค๋ง ์ ์ฉ (D1 ์ ์ถฉ๋ ํ ์ด๋ธ ๊ฐ์ง ๋ฐ ์ฒ๋ฆฌ ํฌํจ) - ์ด๊ธฐ ๊ด๋ฆฌ์ ๊ณ์ ์์ฑ โ ์กฐ์ง๋ช , ๊ด๋ฆฌ์ ๊ณ์ ์ ๋ณด, Issuer URL ์ ๋ ฅ ํ DB์ ์๋ (๋น๋ฐ๋ฒํธ ๋ฏธ์ ๋ ฅ ์ ์๋ ์์ฑ)
- ์๋ช
ํค โ
IDP_SIGNING_KEY_SECRET์๋ ์์ฑ ๋๋ ์ง์ ์ ๋ ฅ ํ.env์ ์ ์ฅ
# ์: PostgreSQL ๋ก ์
์
bun run setup -- --dialect postgres --database-url "postgres://user:pass@host:5432/db" --hyperdrive-id <id>R2 ๋ฒํท(
keystone-skin-cache)์ ์ปค์คํ ๋ก๊ทธ์ธ ์คํจ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ๋ ํ์ํฉ๋๋ค. ์ฌ์ฉํ์ง ์๋๋ค๋ฉดwrangler.jsonc์r2_bucketsํญ๋ชฉ์ ์ฃผ์ ์ฒ๋ฆฌํด๋ ๋ฉ๋๋ค.
์ ์ ์๋ฃ ํ ๋ก์ปฌ ๊ฐ๋ฐ ์๋ฒ๋ฅผ ์์ํฉ๋๋ค:
bun run devbun run deploy๋ฐฐํฌ ์ Wrangler Secret์ผ๋ก ๋ฏผ๊ฐํ ๊ฐ์ ์ค์ ํฉ๋๋ค:
wrangler secret put IDP_SIGNING_KEY_SECRET์ฐธ๊ณ : ๋ก์ปฌ ๊ฐ๋ฐ์์๋
.env์ ํ๋ฌธ์ผ๋ก ์ ์ฅํด๋ ๋ฌด๋ฐฉํ์ง๋ง, ํ๋ก๋์ ์์๋ ๋ฐ๋์ Secret์ผ๋ก ๊ด๋ฆฌํ์ธ์.
| ๋ณ์ | ํ์ | ์ค๋ช |
|---|---|---|
IDP_ISSUER_URL |
โ | OIDC/SAML ๋ฐ๊ธ์ URL (๋ฐฐํฌ ๋๋ฉ์ธ๊ณผ ์ผ์น). ํ๋ก๋์ ํ์ โ ๋ฏธ์ค์ ์ ์์ฒญ ์ด๊ธฐ 503(fail-closed). dev ์์๋ง ์์ฒญ origin ์๋ ๋์ฒด |
IDP_SIGNING_KEY_SECRET |
โ | ์๋ช ํค ์ํธํ KEK (ํ๋ก๋์ ์ ๋ฐ๋์ Secret). ํ๋ก๋์ ํ์ โ ๋ฏธ์ค์ ์ ์์ฒญ ์ด๊ธฐ์ ์ค๋ฅ๋ก ์ฐจ๋จ(fail-fast) |
DISPATCHER_SERVICE_TOKEN |
์ ํ | stardust dispatcher ๊ฐ /api/totp/* ํธ์ถ ์ ์ฌ์ฉํ Bearer ํ ํฐ. ๋ฏธ์ค์ ์ด๋ฉด ํด๋น API 503 |
IDP_DEFAULT_TENANT_NAME |
์ ํ | ๊ธฐ๋ณธ ํ
๋ํธ ์ด๋ฆ (๊ธฐ๋ณธ: My Organization) |
CLOUDFLARE_ACCOUNT_ID |
์ ํ | Cloudflare ๊ณ์ ID (๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ์์ ์ฌ์ฉ) |
CLOUDFLARE_D1_DATABASE_ID |
์ ํ | D1 ๋ฐ์ดํฐ๋ฒ ์ด์ค ID (๋ง์ด๊ทธ๋ ์ด์ ์คํฌ๋ฆฝํธ์์ ์ฌ์ฉ) |
CLOUDFLARE_D1_PREVIEW_DATABASE_ID |
์ ํ | ํ๋ฆฌ๋ทฐ์ฉ D1 ๋ฐ์ดํฐ๋ฒ ์ด์ค ID |
CLOUDFLARE_D1_TOKEN |
์ ํ | D1 API ํ ํฐ (db:migrate ์คํฌ๋ฆฝํธ์์ ์ฌ์ฉ) |
์ฐธ๊ณ : ์ด๊ธฐ ๊ด๋ฆฌ์ ๊ณ์ ์
bun run setup์ด ์์ฑํฉ๋๋ค. ์๋/CI ์๋๊ฐ ํ์ํ๋ฉดIDP_BOOTSTRAP_ADMIN_USERNAME/IDP_BOOTSTRAP_ADMIN_EMAIL/IDP_BOOTSTRAP_ADMIN_PASSWORD(+์ ํIDP_BOOTSTRAP_ADMIN_NAME) ๋ฅผ ์ค์ ํ๊ณbun run db:seed(๋ฐฉ์ธ๋ณ:db:seed:pg๋ฑ)๋ฅผ ์คํํ์ธ์. ๋น๋ํ ํ๊ฒฝ์์๋SEED_RESET=0|1๋ก ์ด๊ธฐํ ์ฌ๋ถ๋ฅผ ์ง์ ํฉ๋๋ค.
| ๋ฐ์ธ๋ฉ | ์ข ๋ฅ | ์ฉ๋ |
|---|---|---|
DB |
D1 | ๋ฉ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค (DB_DIALECT=d1 ์ผ ๋) |
HYPERDRIVE |
Hyperdrive | PostgreSQL/MySQL ์ฐ๊ฒฐ (DB_DIALECT=postgres|mysql ์ผ ๋) |
SKIN_CACHE |
R2 | ์ปค์คํ ๋ก๊ทธ์ธ ์คํจ ์บ์ |
ASSETS |
์ ์ | SvelteKit ๋น๋ ์ฐ์ถ๋ฌผ (.svelte-kit/cloudflare) |
| ๋ช ๋ น | ์ค๋ช |
|---|---|
bun run dev |
Vite ๊ฐ๋ฐ ์๋ฒ ์คํ |
bun run build |
ํ๋ก๋์ ๋น๋ |
bun run preview |
Wrangler๋ก ๋น๋ ์ฐ์ถ๋ฌผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (localhost:4173) |
bun run check |
wrangler types + svelte-check ํ์
๊ฒ์ฌ |
bun run lint |
Prettier + ESLint ๊ฒ์ฌ |
bun run format |
Prettier ์๋ ํฌ๋งท |
bun run gen |
Wrangler ํ๊ฒฝ ํ์ ์ฌ์์ฑ |
bun run db:generate |
Drizzle ๋ง์ด๊ทธ๋ ์ด์ SQL ์์ฑ (D1) |
bun run db:generate:sqlite |
Drizzle ๋ง์ด๊ทธ๋ ์ด์ SQL ์์ฑ (libSQL/SQLite) |
bun run db:generate:pg |
Drizzle ๋ง์ด๊ทธ๋ ์ด์ SQL ์์ฑ (PostgreSQL) |
bun run db:generate:mysql |
Drizzle ๋ง์ด๊ทธ๋ ์ด์ SQL ์์ฑ (MySQL) |
bun run db:migrate |
๋ง์ด๊ทธ๋ ์ด์ ์ ์ฉ + seed ๋ง์ด๊ทธ๋ ์ด์ (D1) |
bun run db:migrate:pg |
๋ง์ด๊ทธ๋ ์ด์ ์ ์ฉ + seed ๋ง์ด๊ทธ๋ ์ด์ (PostgreSQL) |
bun run db:seed |
๊ธฐ๋ณธ ๋ฐ์ดํฐ ์๋ โ ํ ๋ํธ/๊ด๋ฆฌ์/์๋น์ค role (D1) |
bun run db:seed:pg |
๊ธฐ๋ณธ ๋ฐ์ดํฐ ์๋ (PostgreSQL) |
bun run db:studio |
Drizzle Studio ์คํ (์คํค๋ง/๋ฐ์ดํฐ GUI) |
bun run deploy |
Cloudflare Workers ๋ฐฐํฌ |
db:migrate/db:seed/db:push/db:studio๋:pg/:mysql/:sqlite์ ๋ฏธ์ฌ๋ก ๋ฐฉ์ธ๋ณ ์คํ์ด ๊ฐ๋ฅํฉ๋๋ค. postgres/mysql/sqlite ๋DATABASE_URL, D1 ์CLOUDFLARE_*ํ๊ฒฝ๋ณ์๋ฅผ ์ฌ์ฉํฉ๋๋ค.scripts/seed.ts์scripts/seed-migrate.ts๋ ๋ฐฉ์ธ ๋ฌด๊ด(๊ณต์ฉ ํฌํผscripts/lib/db.ts)์ด๋ฉฐ, D1 ์ REST API ๋ก ์ ๊ทผํฉ๋๋ค.
D1(SQLite)ยทlibSQL(SQLite)ยทPostgreSQLยทMySQL ์ ์ง์ํ๋ฉฐ, ๋ฐฐํฌ ๋จ์๋ก ํ๋๋ง ์ฌ์ฉํฉ๋๋ค. D1 ์ ํธ์์ฑ์ด ๋์ง๋ง ์ง์ฐ์ด ํฐ ํธ์ด๋ผ, ๋ฎ์ ์ง์ฐ์ด ํ์ํ๋ฉด Cloudflare Hyperdrive ๋ก PostgreSQL/MySQL ์ ์ฐ๊ฒฐํ๊ฑฐ๋ ์์ฒด DB ์ ์ง๊ฒฐํ ์ ์์ต๋๋ค.
๋ฐฉ์ธ์ DB_DIALECT ํ๊ฒฝ๋ณ์๋ก ์ ํํฉ๋๋ค (d1(๊ธฐ๋ณธ) | sqlite | postgres | mysql). ์ด ๊ฐ์ ๋ฐํ์๋ฟ ์๋๋ผ ๋น๋ยทํ์
์ฒดํฌยท๋ง์ด๊ทธ๋ ์ด์
์์ฑ ์์ ์๋ ์ฐธ์กฐ๋์ด ์คํค๋งยท๋๋ผ์ด๋ฒ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
| ๋ฐฉ์ธ | ๋๋ผ์ด๋ฒ | ์ฐ๊ฒฐ ๋์ |
|---|---|---|
d1 |
drizzle-orm/d1 |
Cloudflare D1 (Workers ์ ์ฉ ๋ฐ์ธ๋ฉ) |
sqlite |
libSQL | ๋ก์ปฌ ํ์ผ(file:) ๋๋ Turso (์์ Node ๋ฑ) |
postgres |
postgres-js | Hyperdrive ๋๋ DATABASE_URL ์ง๊ฒฐ |
mysql |
mysql2 | Hyperdrive ๋๋ DATABASE_URL ์ง๊ฒฐ |
# PostgreSQL ๋ก ๋น๋ยท๋ฐฐํฌ
DB_DIALECT=postgres bun run build
DB_DIALECT=postgres bun run deploy # ๋๋ wrangler deploy
# MySQL ๋ก ๋น๋ยท๋ฐฐํฌ
DB_DIALECT=mysql bun run build
# ๋ก์ปฌ SQLite ํ์ผ(libSQL) ๋ก Node ๊ตฌ๋
BUILD_TARGET=node DB_DIALECT=sqlite DATABASE_URL="file:./keystone.db" bun run build && node build๋์ ๋ฐฉ์:
- ์คํค๋ง๋ ๋ฐฉ์ธ๋ณ๋ก ๋ถ๋ฆฌ๋์ด ์์ต๋๋ค:
schema.sqlite.ts(d1ยทsqlite ๊ณต์ฉ) /schema.pg.ts/schema.mysql.ts. ์ธ ์คํค๋ง๋ ํ ์ด๋ธยท์ปฌ๋ผยท์ธ๋ฑ์ค๋ช ๊ณผ JS ์ถ๋ก ํ์ ์ด ๋์ผํ๊ฒ ์ ์ง๋๋ฉฐ, ๋ฐฐ๋ดschema.ts๊ฐDB_DIALECT์ ๋ง๋ ํ์ผ๋ก ํด์๋ฉ๋๋ค. src/lib/server/db/index.ts์getDb()๊ฐ ๋ฐฉ์ธ์ ๋ง๋ ๋๋ผ์ด๋ฒ(d1 / libSQL / postgres-js / mysql2)๋ฅผ ์ ํํฉ๋๋ค. ๋น๋ ์ ํ์ฑ ๋ฐฉ์ธ์ ๋๋ผ์ด๋ฒ๋ง ๋ฒ๋ค์ ํฌํจ๋ฉ๋๋ค.- ์ฐ๊ฒฐ ๋ฌธ์์ด ์ฐ์ ์์:
sqlite๋DATABASE_URL/SQLITE_URL(file:์คํด ์์ผ๋ฉด ๋ก์ปฌ ํ์ผ๋ก ๊ฐ์ฃผ).postgres/mysql์ Cloudflare ์์HYPERDRIVE๋ฐ์ธ๋ฉ โDATABASE_URL(var/secret), ์์ Node ์์DATABASE_URL.
# (Cloudflare) Hyperdrive ๊ตฌ์ฑ ์์ฑ (binding ์ด๋ฆ์ ๋ฐ๋์ HYPERDRIVE)
wrangler hyperdrive create keystone-pg --connection-string="postgres://user:pass@host:5432/db"
wrangler hyperdrive create keystone-mysql --connection-string="mysql://user:pass@host:3306/db"
# โ ์ถ๋ ฅ๋ id ๋ฅผ wrangler.jsonc ์ hyperdrive[].id ์ ์ฑ์ฐ๊ณ ์ฃผ์ ํด์
# (๋๋ bun run setup -- --dialect postgres --hyperdrive-id <id> ๋ก ์๋ ์ค์ )
# (Hyperdrive ์์ด ์ง๊ฒฐํ๋ ค๋ฉด DATABASE_URL ์ var/secret ์ผ๋ก ์ค์ )
# ์คํค๋ง ์ ์ฉ + ์๋ (PostgreSQL ์์ โ DATABASE_URL ์ฌ์ฉ)
bun run db:generate:pg && bun run db:migrate:pg
bun run db:seed:pgBUILD_TARGET(cloudflare(๊ธฐ๋ณธ) | node)์ผ๋ก ์ด๋ํฐ๋ฅผ ์ ํํฉ๋๋ค. node ๋ @sveltejs/adapter-node ๋ก ๋น๋๋์ด Cloudflare ์์ด ์์ Node ์๋ฒ๋ก ๊ตฌ๋๋ฉ๋๋ค. Node ์๋ platform ๋ฐ์ธ๋ฉ์ด ์์ผ๋ฏ๋ก PostgreSQL/MySQL/sqlite(libSQL) ๋ฅผ ์ฌ์ฉํ๋ฉฐ(D1 ์ Workers ์ ์ฉ), ์ฐ๊ฒฐ์ DATABASE_URL, ์ค์ ๊ฐ์ process.env ๋ก ์ฝ์ต๋๋ค.
# PostgreSQL + ์์ Node ๋ก ๋น๋ยท๊ตฌ๋
export BUILD_TARGET=node DB_DIALECT=postgres
export DATABASE_URL="postgres://user:pass@host:5432/db"
# ํ์ํ ์ค์ ๋ ํ๊ฒฝ๋ณ์๋ก: IDP_SIGNING_KEY_SECRET, IDP_ISSUER_URL, IDP_DEFAULT_TENANT_NAME ...
bun run build
node build # adapter-node ์ฐ์ถ๋ฌผ (๊ธฐ๋ณธ PORT=3000)์ฐธ๊ณ : MySQL ์
UPDATE ... RETURNING๊ณผ partial(๋ถ๋ถ) unique index ๋ฅผ ์ง์ํ์ง ์์ต๋๋ค. ํด๋น ๋ก์ง(์ธ๊ฐ ์ฝ๋/WebAuthn challenge ์์ง, signing key ํ์ , rate limit upsert)์ ๋ฐฉ์ธ๋ณ๋ก ๋ถ๊ธฐ๋์ด MySQL ์์๋affectedRowsํ์ ยท์ฌ์กฐํ์ ํธ๋์ญ์ ์ผ๋ก ๋๋ฑํ๊ฒ ๋์ํฉ๋๋ค.
์ปค์คํ ๋ก๊ทธ์ธ ์คํจ ์บ์๋ ๋ค์ ์ฐ์ ์์๋ก ์คํ ๋ฆฌ์ง๋ฅผ ์ ํํฉ๋๋ค (๋ฏธ์ค์ ์ ์บ์ ์์ด ๋งค๋ฒ ์๋ณธ fetch โ ์ ์ ๋์).
- Cloudflare R2 ๋ฐ์ธ๋ฉ(
SKIN_CACHE) ์ด ์์ผ๋ฉด R2 ์ฌ์ฉ. - ์๊ณ S3 ํธํ ์ค์ (
S3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY)์ด ์์ผ๋ฉด S3 ํธํ ์คํ ๋ฆฌ์ง ์ฌ์ฉ.
R2 ์์ฒด๊ฐ S3 ํธํ์ด๋ฏ๋ก AWS S3ยทMinIOยทCeph ๋ ๋ฌผ๋ก R2 ์ S3 endpoint ๋ ๊ทธ๋๋ก ์ธ ์ ์์ต๋๋ค. ์๋ช
์ aws4fetch ๋ก ํ๋ฉฐ Workers/Node ์์ชฝ์์ ๋์ํฉ๋๋ค. path-style/virtual-host ๋ S3_FORCE_PATH_STYLE(๊ธฐ๋ณธ true, MinIO/R2 ๊ถ์ฅ)๋ก ์ ์ดํฉ๋๋ค. ์์ธํ ๋ณ์๋ .env.example ์ฐธ๊ณ .
์คํค๋ง ๋ณ๊ฒฝ โ ๋ง์ด๊ทธ๋ ์ด์ ์์ฑ โ ์ ์ฉ์ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
# 1. ์ฌ์ฉํ๋ ๋ฐฉ์ธ์ ์คํค๋ง ํ์ผ ์์
# - D1 / sqlite โ src/lib/server/db/schema.sqlite.ts (๊ณต์ฉ)
# - PostgreSQL โ src/lib/server/db/schema.pg.ts
# - MySQL โ src/lib/server/db/schema.mysql.ts
# (์ธ ์คํค๋ง์ ๊ตฌ์กฐ/ํ์
์ ๋์ผํ๊ฒ ์ ์งํ ๊ฒ)
# 2. SQL ์์ฑ (์ฌ์ฉํ๋ ๋ฐฉ์ธ์ ๋ง๊ฒ)
bun run db:generate # D1 โ drizzle/*.sql
bun run db:generate:sqlite # libSQL โ drizzle/sqlite/*.sql (DDL ์ D1 ๊ณผ ๋์ผ)
bun run db:generate:pg # PostgreSQL โ drizzle/pg/*.sql
bun run db:generate:mysql # MySQL โ drizzle/mysql/*.sql
# 3. ์์ฑ๋ SQL ํ์ผ ๊ฒํ
# 4. ์๊ฒฉ DB์ ์ ์ฉ (์ฌ์ฉ์๊ฐ ์ง์ ์คํ)
bun run db:migrate # D1 ํ๋ก๋์
bun run db:migrate:preview # D1 ํ๋ฆฌ๋ทฐ
# PostgreSQL/MySQL ์ DATABASE_URL ์ค์ ํ drizzle-kit migrate ๋ก ์ ์ฉ
โ ๏ธ ์๊ฒฉ D1์ ๋ํ ๋ง์ด๊ทธ๋ ์ด์ ์ ์ฉ์ ๋๋๋ฆฌ๊ธฐ ์ด๋ ค์ฐ๋ฏ๋ก, ์๋ํ๋ ์คํฌ๋ฆฝํธ๋ ์์ด์ ํธ๊ฐ ์์๋ก ์คํํ์ง ์๋๋ก ์ด์ ์ ์ฑ ์ ํฌํจ์ํค๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
์ ๊ท ๋น๋ฐ๋ฒํธ๋ argon2id (@hicaru/argon2-pure.js, Workers ํธํ ์์ JS ๊ตฌํ)๋ก ํด์ฑ๋ฉ๋๋ค. ๊ณผ๊ฑฐ PBKDF2-SHA256 (100,000 iterations)์ผ๋ก ์ ์ฅ๋ ๋ ๊ฑฐ์ ํด์๋ ๋ก๊ทธ์ธ ๊ฒ์ฆ ์ ์๋์ผ๋ก argon2id๋ก ์ฌํด์ฑ๋ฉ๋๋ค.
OIDC ID Token ๋ฐ SAML Response ์๋ช
์ ์ฌ์ฉ๋๋ RSA ํค๋ IDP_SIGNING_KEY_SECRET์ผ๋ก ์ํธํ๋์ด D1์ ์ ์ฅ๋ฉ๋๋ค. ์ด ์ํฌ๋ฆฟ์ด ์ ์ถ๋๋ฉด ๋ชจ๋ ์๋ช
ํค๊ฐ ๋ณตํธํ๋ ์ ์์ผ๋ฏ๋ก ๋ฐ๋์ ๊ฐํ ๋๋ค๊ฐ(openssl rand -base64 32)์ ์ฌ์ฉํ๊ณ ์ ๊ธฐ์ ์ผ๋ก ๊ต์ฒดํ์ธ์. ํค ํ์ ์ ๊ด๋ฆฌ์ UI(/admin/signing-keys)์์ ์ํํ ์ ์์ต๋๋ค.
WebAuthn ๋ฑ๋กยท์ธ์ฆ challenge๋ D1์ 1ํ์ฉ์ผ๋ก ์ ์ฅ๋๋ฉฐ, ์์ง ์ฆ์ ์ญ์ ๋ฉ๋๋ค. challenge๋ ํ ๋ํธ ID๋ก ๊ฒฉ๋ฆฌ๋์ด ๋ค๋ฅธ ํ ๋ํธ์ challenge๋ฅผ ์ฌ์ฌ์ฉํ ์ ์์ต๋๋ค.
LDAP ์ธ์ฆ ์ฑ๊ณต ์, ๋์ผ ์ด๋ฉ์ผ์ ๊ธฐ์กด ๋ก์ปฌ ๊ณ์ ์ด ์๋ ๊ฒฝ์ฐ ์๋ ์ฐ๊ฒฐํ์ง ์์ต๋๋ค. LDAP ํ๋ก๋ฐ์ด๋๊ฐ ์ด๋ฉ์ผ์ ์กฐ์ํด ๊ธฐ์กด ๊ด๋ฆฌ์ ๊ณ์ ์ ํ์ทจํ๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํจ์ ๋๋ค. ๊ธฐ์กด ๋ก์ปฌ ๊ณ์ ๊ณผ์ ์ฐ๊ฒฐ์ ๊ด๋ฆฌ์๊ฐ ์ง์ ์ํํด์ผ ํฉ๋๋ค.
hooks.server.ts์์ ๋ชจ๋ ์๋ต์ ๋ค์ ํค๋๋ฅผ ์ ์ฉํฉ๋๋ค.
X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originStrict-Transport-Security(HSTS)Permissions-Policy(camera/microphone/geolocation/payment ๋นํ์ฑ)- ํด์ ๊ธฐ๋ฐ
Content-Security-Policy
์ด๊ธฐ ๊ด๋ฆฌ์ ๊ณ์ ์ bun run setup ์คํ ์ D1์ ์ง์ ์ฝ์
๋ฉ๋๋ค. ์
์
์๋ฃ ํ ๊ฐ๋ฅํ ๋นจ๋ฆฌ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ณ๊ฒฝํ๊ณ MFA๋ฅผ ์ค์ ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.