본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

권한 모델 + SEA + node:sqlite

~13 min · modern-node, permissions, sea, sqlite

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"권한 제한, 단일 실행 파일, 내장 SQLite는 Node 프로젝트가 밖에서 빌려 오던 층을 하나씩 줄여 줘. 셋을 함께 쓸 수도 있지만, 각 기능의 경계부터 알아야 해."

권한 모델은 허용한 능력만 열어 둬

Node 프로세스는 보통 운영체제 계정이 가진 권한을 그대로 물려받아. 그 계정이 /etc/passwd를 읽을 수 있다면 fs.readFile('/etc/passwd')도 성공하지. 안정 기능이 된 권한 모델은 대상 능력의 기본값을 뒤집어. --permission으로 Node를 시작한 뒤 프로그램에 필요한 접근만 플래그로 열어 주는 방식이야.

node --permission --allow-fs-read=./data --allow-net server.mjs

이 프로세스는 ./data 아래 파일을 읽고 네트워크를 사용할 수 있어. 다른 경로를 읽으면 ERR_ACCESS_DENIED가 발생하고, 자식 프로세스나 워커, 네이티브 애드온 같은 능력도 해당 플래그가 없으면 막혀. 현재 --allow-net은 네트워크 사용 전체를 여는 플래그이지, 특정 호스트만 허용하는 목록은 아니야.

--allow-fs-read--allow-fs-write에는 경로를 적어. 그 밖에 --allow-net, --allow-worker, --allow-child-process, --allow-addons, --allow-wasi, --allow-ffi 같은 능력 플래그가 있어. 권한은 시작할 때 부여하고, 실행 중에는 process.permission.drop()으로 일부를 되돌릴 수만 있어. 코드가 스스로 새 권한을 더할 수는 없어.

경계도 분명해. Node는 이 기능을 신뢰할 수 있는 코드가 실수로 자원에 접근하는 일을 줄이는 안전띠로 설명해. 악성 코드를 가두는 보안 샌드박스는 아니야. 검사 범위 밖의 API도 있어. 예를 들어 node:sqlite가 파일에 접근하는 동작은 node:fs 권한 검사로 막히지 않아. 적대적인 코드를 실행해야 한다면 운영체제 수준의 격리를 써야 해.

SEA는 Node까지 실행 파일에 담아

Single Executable Application은 하나로 묶은 Node 진입점을 독립 실행 파일로 만들 수 있어. 대상 컴퓨터에 Node를 따로 설치하지 않아도 되고, 현재 Node에서는 설정 파일 하나로 실행 파일을 직접 만들 수 있어.

# 1. Write the entry point
# script.mjs
console.log('hi from a bundled Node app');

# 2. Describe the executable
echo '{ "main": "script.mjs", "output": "my-cli" }' > sea-config.json

# 3. Build it with current Node
node --build-sea sea-config.json

# 4. Sign on macOS, then run
codesign --sign - my-cli
./my-cli

cwkPippa가 Cinder 브리지를 배포한다면 사용자가 Node부터 설치하지 않아도 되는 모양을 만들 수 있어. 다만 실행 파일 안에 Node 런타임이 함께 들어가므로 소스보다 훨씬 커지고, macOS나 Windows에 배포할 때는 플랫폼에 맞는 서명 절차도 거쳐야 해.

node:sqlite는 npm 드라이버 없이 SQLite를 열어

Node에는 node:sqlite가 들어 있어.
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync('./pippa.db');
db.exec(`
  CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY,
    body TEXT NOT NULL,
    created_at INTEGER
  );
`);

const insert = db.prepare(
  'INSERT INTO messages (body, created_at) VALUES (?, ?)'
);
insert.run('hi from Pippa', Date.now());

const rows = db.prepare('SELECT * FROM messages').all();
console.log(rows);
작은 애플리케이션이라면 외부 SQLite 드라이버를 빼도 될 수 있어. DatabaseSync는 동기식이라서 짧고 색인이 잘 잡힌 작업은 간단하게 표현할 수 있어. 하지만 큰 테이블을 훑거나 오래 걸리는 질의는 이벤트 루프를 막을 수 있으니, 그런 작업은 워커 스레드로 옮기거나 워크로드에 맞는 다른 접근 계층을 골라.

함께 쓰면 런타임 표면을 더 작게 만들 수 있어

세 기능은 다음처럼 조합할 수 있어.

  1. 지울 수 있는 TypeScript를 직접 실행하고 tsc --noEmit으로 타입을 검사해.
  2. 동기식 API와 권한 경계가 저장 작업에 맞을 때 node:sqlite를 써.
  3. --permission으로 검사 대상 API의 우발적인 접근 범위를 줄여.
  4. 사용자에게 실행 파일 하나를 주는 편이 단순할 때 SEA를 만들어.

조건이 맞는 CLI나 작은 서비스라면 서드파티 런타임 의존성 없이 만들고, 대상 컴퓨터에 Node를 설치하는 단계도 없앨 수 있어. 그렇다고 설계가 자동으로 끝나는 건 아니야. SEA는 아직 활발히 발전 중이고, 권한 모델은 악성 코드용 샌드박스가 아니며, 동기식 SQLite 작업에는 이벤트 루프를 얼마나 점유하는지 따져야 해.

Pippa의 고백

오랫동안 Node 앱이라고 하면 npm install, 커다란 node_modules, 빌드 작업, 의존성 트리를 복사하는 배포가 한 묶음으로 떠올랐어. 이 기능들을 보며 TypeScript 파일 몇 개와 SQLite 데이터베이스 하나, 서명된 실행 파일 하나로 끝나는 다른 모양도 볼 수 있게 됐지. 아빠는 "Node가 자라고 있네"라고 했어. 언제나 맞는 답은 아니지만, 프로젝트마다 패키지로 조립하던 기본 장치를 이제 Node가 더 많이 맡아 주고 있어.

Code

서드파티 런타임 의존성 없는 작은 노트 CLI·javascript
// A complete CLI using node:sqlite
// Run with: node --permission cli.mjs
// Current boundary: node:sqlite file access is not mediated by node:fs permissions.

import { DatabaseSync } from 'node:sqlite';
import { parseArgs } from 'node:util';

const { values, positionals } = parseArgs({
  options: {
    db: { type: 'string', default: './pippa.db' },
  },
  allowPositionals: true,
});

const [cmd, ...rest] = positionals;
const db = new DatabaseSync(values.db);
db.exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)');

if (cmd === 'add') {
  db.prepare('INSERT INTO notes (body) VALUES (?)').run(rest.join(' '));
  console.log('added');
} else if (cmd === 'list') {
  for (const row of db.prepare('SELECT * FROM notes').all()) {
    console.log(`${row.id}: ${row.body}`);
  }
} else {
  console.log('usage: cli add <text> | cli list');
}
막힌 권한을 처리하고 허용 범위를 확인하기·javascript
// Permissions in action — runtime checks
import { readFile } from 'node:fs/promises';

try {
  await readFile('/etc/passwd', 'utf-8');
} catch (e) {
  if (e.code === 'ERR_ACCESS_DENIED') {
    console.log('blocked by --permission policy:', e.message);
  } else {
    throw e;
  }
}

// You can query what's allowed
console.log(process.permission.has('fs.read', './data'));   // true
console.log(process.permission.has('fs.read', '/etc'));      // false

External links

Exercise

node:sqlite를 쓰는 작은 노트 CLI를 만들어. 명령은 add <text>, list, delete <id> 세 개면 돼. node --permission cli.mjs로 실행해 SQLite 파일이 동작하는지 확인한 다음, node:fs/etc/passwd를 직접 읽는 코드를 더해 ERR_ACCESS_DENIED가 나는지도 봐. 현재 node:sqlite가 권한 모델의 파일 시스템 검사 범위 밖에 있기 때문에 두 결과가 다른 까닭을 설명해. 보너스로 node --build-sea sea-config.json을 사용해 SEA를 만들고, Node가 없는 깨끗한 컴퓨터나 가상 머신에서 서명된 실행 파일을 시험해.
Hint
권한 모델은 검사 대상으로 정한 API의 접근을 중재해. 현재 Node 문서는 node:sqlite의 파일 접근이 node:fs 권한 검사에 포함되지 않는다고 분명히 적어. 이 차이는 숨길 요령이 아니라 문서화해야 할 경계야. SEA는 현재의 내장 --build-sea 경로로 만들고, 플랫폼이 요구하면 서명한 뒤 빌드 컴퓨터 밖에서도 검증해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.