EmDash는 플러그인을 trusted와 sandboxed 두 가지 실행 모드로 돌릴 수 있습니다. 이 페이지에서는 각 모드의 동작, 제공하는 보호, 배포 대상별 보안 영향을 설명합니다.
실행 모드
| Trusted | Sandboxed | |
|---|---|---|
| 실행 위치 | 메인 프로세스 | 격리된 V8 isolate(Dynamic Worker Loader) |
| Capabilities | 참고용(강제되지 않음) | 런타임에 강제 |
| 리소스 제한 | 없음 | CPU, 메모리, 하위 요청, 경과 시간 |
| 네트워크 | 제한 없음 | 차단됨. ctx.http와 호스트 허용 목록만 |
| 데이터 접근 | DB 전체 접근 | 선언한 capabilities를 통한 RPC 브리지로 제한 |
| 사용 가능 | 모든 플랫폼 | Cloudflare Workers만 |
Trusted 모드
Trusted 플러그인은 Astro 사이트와 같은 프로세스에서 실행됩니다. npm 패키지나 로컬 파일에서 로드되며 astro.config.mjs에서 설정합니다.
import myPlugin from "@emdash-cms/plugin-analytics";
export default defineConfig({
integrations: [
emdash({
plugins: [myPlugin()],
}),
],
});
Trusted 모드에서:
- Capabilities는 문서일 뿐 강제되지 않습니다.
["read:content"]를 선언해도 프로세스 내 모든 것에 접근할 수 있습니다.capabilities필드는 관리자에게 플러그인이 의도하는 사용 범위를 알려 줍니다. - 리소스 제한이 없습니다. CPU, 메모리, 네트워크 사용량에 상한이 없어 문제 있는 플러그인이 전체 요청을 멈출 수 있습니다.
- 프로세스 전체 접근. 플러그인은 Astro 사이트와 동일한 Node.js/Workers 런타임을 공유합니다. 임의 모듈 import, 환경 변수 접근, (Node.js에서는) 파일 시스템 읽기/쓰기가 가능합니다.
Sandboxed 모드(Cloudflare Workers)
Sandboxed 플러그인은 Cloudflare의 Dynamic Worker Loader API가 제공하는 격리 V8 isolate에서 실행됩니다. 각 플러그인은 제한이 적용된 자체 isolate를 갖습니다.
샌드박싱을 켜려면 Astro 설정에서 sandbox runner를 구성합니다.
export default defineConfig({
integrations: [
emdash({
sandboxRunner: "@emdash-cms/cloudflare/sandbox",
sandboxed: [
{
manifest: seoPluginManifest,
code: seoPluginCode,
},
],
}),
],
});
샌드박스가 강제하는 것
-
Capability 강제
플러그인이
capabilities: ["read:content"]를 선언하면ctx.content.get()과ctx.content.list()만 호출할 수 있습니다.ctx.content.create()를 시도하면 권한 오류가 납니다. RPC 브리지가 강제하며, DB에 직접 접근할 수 없어 우회할 수 없습니다. -
리소스 제한
각 호출(훅 또는 라우트)은 다음과 함께 실행됩니다.
리소스 기본값 강제 주체 CPU 시간 50ms Worker Loader(V8 isolate) 하위 요청 호출당 10 Worker Loader(V8 isolate) 경과 시간 30초 EmDash 러너( Promise.race)메모리 ~128MB V8 플랫폼 상한(플러그인별 설정 불가) CPU 또는 하위 요청 한도 초과 시 Worker Loader가 isolate를 중단하고 예외를 던집니다. 경과 시간 초과 시 EmDash가 호출 Promise를 거부합니다. 메모리는 V8 상한으로 제한되지만 플러그인별로는 설정할 수 없습니다.
위는 내장 기본값입니다.
SandboxRunnerFactory로SandboxOptions.limits에 다른 값을 넘기면 사용자 정의 제한을 줄 수 있습니다. EmDash 통합 설정을 통한 사이트별 구성은 아직 없습니다. -
네트워크 격리
Sandboxed 플러그인은
globalOutbound: null이어서 직접fetch()호출이 V8 수준에서 차단됩니다.ctx.http.fetch()를 사용해야 하며 브리지를 통해 프록시됩니다. 브리지는 대상 호스트를 플러그인의allowedHosts와 대조합니다. -
스토리지 범위
KV, 컬렉션 등 모든 스토리지 작업은 플러그인 ID로 범위가 제한됩니다. 다른 플러그인 데이터는 읽을 수 없습니다. 콘텐츠·미디어 접근은 브리지를 거치며 호출마다 capability를 검사합니다.
-
기능 제한
다음은 Trusted 모드에서만 사용할 수 있습니다.
- API routes — 사용자 정의 REST 엔드포인트(
routes)는 사용할 수 없습니다. Sandboxed 플러그인은 Block Kit 관리 페이지와 훅으로 상호작용합니다. - Portable Text 블록 타입 — PT 블록은 사이트 측 렌더링용 Astro 컴포넌트(
componentsEntry)가 필요하며 빌드 시 npm에서 로드됩니다. Sandboxed 플러그인은 런타임에 설치되어 컴포넌트를 포함할 수 없습니다. - 사용자 정의 React 관리 페이지 — Sandboxed 플러그인은 React 컴포넌트 대신 Block Kit로 관리 UI를 구성합니다.
플러그인이 이런 기능을 선언하면
emdash plugin bundle이 경고합니다. - API routes — 사용자 정의 REST 엔드포인트(
아키텍처
Sandboxed 플러그인은 RPC 브리지로 EmDash와 통신합니다.
┌─────────────────────┐ RPC ┌──────────────────────┐
│ Plugin Isolate │ ◄──────────► │ PluginBridge │
│ (Worker Loader) │ (binding) │ (WorkerEntrypoint) │
│ │ │ │
│ ctx.kv.get(k) │──────────────│► kvGet(k) │
│ ctx.content.list() │──────────────│► contentList() │
│ ctx.http.fetch(u) │──────────────│► httpFetch(u) │
└─────────────────────┘ └──────────────────────┘
│
▼
┌──────────────┐
│ D1 / R2 │
└──────────────┘
플러그인 코드는 V8 isolate에서 실행되며, ctx의 각 메서드는 브리지로의 프록시입니다. 브리지는 메인 EmDash worker에서 동작하며 capability 검증 후 실제 DB/스토리지 작업을 수행합니다.
Wrangler 구성
샌드박싱에는 Dynamic Worker Loader가 필요합니다. wrangler.jsonc에 추가하세요.
{
"worker_loaders": [{ "binding": "LOADER" }],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "emdash-media" }],
"d1_databases": [{ "binding": "DB", "database_name": "emdash" }]
}
Node.js 배포
Node.js(또는 Cloudflare가 아닌 플랫폼)에 배포할 때:
NoopSandboxRunner가 사용되며isAvailable() === false를 반환합니다.- Sandboxed 플러그인 로드를 시도하면
SandboxNotAvailableError가 발생합니다. - 모든 플러그인은
plugins배열에 Trusted로 등록해야 합니다. - capability 선언은 정보용일 뿐 강제되지 않습니다.
보안상 의미
| 위협 | Cloudflare(Sandboxed) | Node.js(Trusted만) |
|---|---|---|
| 허용되지 않은 데이터 읽기 | 브리지 capability 검사로 차단 | 막지 못함 — DB 전체 접근 |
| 무단 네트워크 호출 | globalOutbound: null + 호스트 허용으로 차단 | 막지 못함 — 직접 fetch() 가능 |
| CPU 고갈 | Worker Loader가 isolate 중단 | 막지 못함 — 이벤트 루프 블로킹 |
| 메모리 고갈 | Worker Loader가 isolate 종료 | 막지 못함 — 프로세스 크래시 가능 |
| 환경 변수 접근 | 없음(격리 V8 컨텍스트) | 막지 못함 — process.env 공유 |
| 파일 시스템 접근 | Workers에는 FS 없음 | 막지 못함 — fs 전체 접근 |
Node.js 배포 권장 사항
- 신뢰할 수 있는 출처의 플러그인만 설치. 설치 전 소스 코드를 검토하고 알려진 유지 관리자의 플러그인을 선호하세요.
- Capability를 리뷰 체크리스트로 사용. 강제되지 않아도 의도된 범위를 문서화합니다. 네트워크가 필요 없는데
["network:fetch"]를 선언한 것은 수상합니다. - 리소스 사용을 모니터링. 프로세스 수준 모니터링(예:
--max-old-space-size, 헬스 체크)으로 비정상 플러그인을 잡으세요. - 신뢰할 수 없는 플러그인은 Cloudflare 고려. 출처를 알 수 없는 플러그인(마켓플레이스 등)을 실행해야 한다면 샌드박스가 있는 Cloudflare Workers에 배포하세요.
API는 같고 보장은 다름
실행 모드와 관계없이 플러그인 코드는 동일합니다. definePlugin() API, ctx 형태, hooks, routes, storage 사용은 같고, 달라지는 것은 강제 여부입니다.
// This plugin works in both trusted and sandboxed mode
export default definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["read:content", "network:fetch"],
allowedHosts: ["api.analytics.example.com"],
hooks: {
"content:afterSave": async (event, ctx) => {
// In trusted mode: ctx.http is always present (capabilities not enforced)
// In sandboxed mode: ctx.http is present because "network:fetch" is declared
await ctx.http.fetch("https://api.analytics.example.com/track", {
method: "POST",
body: JSON.stringify({ contentId: event.content.id }),
});
},
},
});
로컬에서는 Trusted로 빠르게 개발·디버깅하고, 프로덕션에서는 코드 변경 없이 Sandboxed로 배포하는 것이 목표입니다.