Deno의 권한 기반 보안 모델을 심층 분석합니다. 각 권한 플래그의 동작 원리, Node.js와의 보안 비교, 공급망 공격 방어, 그리고 실무 보안 모범 사례를 다룹니다.
대부분의 프로그래밍 런타임은 "모든 것을 허용"하는 방식으로 동작합니다. Node.js에서 node script.js를 실행하면, 해당 스크립트는 파일 시스템 전체, 모든 네트워크, 환경 변수, 하위 프로세스에 자유롭게 접근할 수 있습니다. 이것은 개발 편의성 면에서는 좋지만, 보안 관점에서는 심각한 문제입니다.
Deno는 이 패러다임을 뒤집었습니다. 기본적으로 아무것도 허용하지 않고, 필요한 권한만 명시적으로 부여하는 모델을 채택했습니다. 이것은 운영체제의 권한 시스템이나 모바일 앱의 권한 요청 방식과 유사한 접근입니다.
# Node.js: 모든 권한이 기본 허용
node server.js
# Deno: 필요한 권한만 명시적으로 부여
deno run --allow-net=0.0.0.0:8080 --allow-read=./public server.ts이 차이가 실무에서 어떤 의미를 가지는지, 각 권한 플래그는 어떻게 동작하는지, 그리고 실제 보안 위협을 어떻게 방어하는지를 이 장에서 상세히 살펴봅니다.
Deno 2에서 사용할 수 있는 권한 플래그는 다음과 같습니다.
--allow-read[=<경로>...] 파일 시스템 읽기
--allow-write[=<경로>...] 파일 시스템 쓰기
--allow-net[=<호스트:포트>...] 네트워크 접근
--allow-env[=<변수명>...] 환경 변수 읽기
--allow-run[=<프로그램>...] 하위 프로세스 실행
--allow-ffi 외부 함수 인터페이스 (동적 라이브러리)
--allow-sys[=<API>...] 시스템 정보 접근
--allow-all (또는 -A) 모든 권한 허용각 플래그는 세밀한 범위 지정을 지원합니다. 단순히 "허용/거부"가 아니라, 어떤 대상에 대해 허용하는지를 구체적으로 명시할 수 있습니다.
파일 시스템 권한은 읽기(--allow-read)와 쓰기(--allow-write)로 분리되어 있으며, 접근 가능한 경로를 제한할 수 있습니다.
// --allow-read=./data,./config 로 실행한 경우
// 허용: ./data 디렉토리 내 파일 읽기
const data = await Deno.readTextFile("./data/users.json");
// 허용: ./config 디렉토리 내 파일 읽기
const config = await Deno.readTextFile("./config/app.toml");
// 거부: 다른 경로 접근 시 PermissionDenied 에러
try {
await Deno.readTextFile("/etc/passwd");
} catch (e) {
// Deno.errors.PermissionDenied: Requires read access to "/etc/passwd"
console.error(e.message);
}--allow-read를 경로 지정 없이 사용하면 파일 시스템 전체에 대한 읽기가 허용됩니다. 보안을 위해 가능한 한 구체적인 경로를 지정하는 것을 권장합니다.
네트워크 권한은 호스트와 포트 단위로 세밀하게 제어할 수 있습니다.
# 특정 호스트만 허용
deno run --allow-net=api.example.com script.ts
# 특정 호스트의 특정 포트만 허용
deno run --allow-net=api.example.com:443 script.ts
# 여러 호스트 허용
deno run --allow-net=api.example.com,cdn.example.com script.ts
# 로컬 서버 바인딩만 허용
deno run --allow-net=0.0.0.0:8080 server.ts이 세분화된 네트워크 권한은 데이터 유출을 방지하는 데 매우 효과적입니다. 예를 들어, 빌드 도구가 api.example.com에만 접근하도록 제한하면, 악성 코드가 다른 서버로 데이터를 전송하는 것을 원천 차단할 수 있습니다.
// --allow-env=DATABASE_URL,API_KEY 로 실행
// 허용
const dbUrl = Deno.env.get("DATABASE_URL");
const apiKey = Deno.env.get("API_KEY");
// 거부: 허용되지 않은 환경 변수
const secret = Deno.env.get("AWS_SECRET_KEY"); // PermissionDenied환경 변수에 저장된 API 키, 데이터베이스 비밀번호 등 민감 정보를 보호하는 데 유용합니다. 특히 서드파티 패키지가 불필요한 환경 변수에 접근하는 것을 방지할 수 있습니다.
# 특정 프로그램만 실행 허용
deno run --allow-run=git,deno script.ts
# 실행이 허용된 명령만 사용 가능
const cmd = new Deno.Command("git", {
args: ["status"]
});
const output = await cmd.output(); // 허용
const dangerous = new Deno.Command("rm", {
args: ["-rf", "/"]
});
// PermissionDenied: Requires run access to "rm"--allow-run은 보안 관점에서 가장 민감한 권한 중 하나입니다. 하위 프로세스는 Deno의 샌드박스를 벗어나 시스템 명령을 직접 실행하기 때문입니다. 반드시 실행할 프로그램을 명시적으로 지정하세요.
권한 검사는 Deno 런타임 내부의 ops 레이어에서 수행됩니다.
모든 시스템 접근 API는 실제 작업을 수행하기 전에 권한 검사를 거칩니다. 이 검사는 Rust 코드 수준에서 수행되므로, JavaScript 코드에서 우회할 수 없습니다.
Deno는 실행 중에 사용자에게 권한을 요청하는 것도 지원합니다. 이를 프롬프트(Prompt) 방식이라고 합니다.
// 런타임에 권한 상태 확인
const status = await Deno.permissions.query({
name: "read",
path: "./data.json",
});
if (status.state === "prompt") {
// 사용자에게 권한 요청 (터미널에 프롬프트 표시)
const granted = await Deno.permissions.request({
name: "read",
path: "./data.json",
});
if (granted.state === "granted") {
const data = await Deno.readTextFile("./data.json");
console.log(data);
}
}--no-prompt 플래그를 사용하면 프롬프트를 비활성화할 수 있습니다. CI/CD 환경에서는 이 플래그를 사용하여 모든 권한을 사전에 명시하는 것이 좋습니다. 프롬프트가 비활성화된 상태에서 권한이 없으면 즉시 에러가 발생합니다.
한 번 부여된 권한을 런타임에서 취소할 수도 있습니다.
// 설정 파일을 읽은 후 읽기 권한 취소
const config = await Deno.readTextFile("./config.json");
// 이후 파일 읽기가 필요 없으므로 권한 취소
await Deno.permissions.revoke({ name: "read" });
// 이후 파일 읽기 시도 시 PermissionDenied이 패턴은 "최소 권한 원칙(Principle of Least Privilege)"을 런타임 수준에서 구현한 것입니다. 초기화 단계에서 필요한 자원을 읽은 후 해당 권한을 취소하면, 이후 코드 실행 중 발생할 수 있는 보안 위협을 줄일 수 있습니다.
Node.js 생태계에서 실제로 발생한 공급망 공격(Supply Chain Attack) 사례를 통해 Deno의 보안 모델이 어떤 차이를 만드는지 살펴봅니다.
Node.js에서는 패키지를 설치하는 것만으로도 postinstall 스크립트를 통해 임의의 코드가 실행될 수 있습니다. 이 코드는 시스템의 모든 자원에 접근 가능합니다.
Deno에서는 동일한 악성 코드가 실행되더라도, 명시적으로 부여한 권한 범위 내에서만 동작할 수 있습니다. 네트워크 접근 권한이 없으면 데이터를 외부로 전송할 수 없고, 환경 변수 접근 권한이 없으면 민감 정보를 읽을 수 없습니다.
event-stream 사건 (2018)
- 인기 패키지의 관리 권한 탈취
- 특정 비트코인 지갑 앱을 노리는 악성 코드 삽입
- 수백만 다운로드에 영향
ua-parser-js 사건 (2021)
- 주간 800만 다운로드 패키지
- 크립토마이너와 패스워드 스틸러 삽입
- npm 계정 탈취를 통한 공격
colors/faker 사건 (2022)
- 패키지 관리자의 의도적 파괴
- 무한 루프 코드 삽입
- 수많은 의존성 체인에 영향이러한 공급망 공격은 Node.js의 보안 모델 부재와 직접적으로 관련됩니다. Deno의 권한 시스템이 모든 공격을 막을 수는 없지만, 공격의 영향 범위를 크게 제한할 수 있습니다.
Node.js도 이러한 보안 문제를 인식하고 실험적 기능으로 Permission Model을 도입했습니다.
# Node.js 20+에서 실험적으로 사용 가능
node --experimental-permission --allow-fs-read=./data server.js그러나 Node.js의 Permission Model은 몇 가지 한계가 있습니다. 실험적 기능으로 프로덕션 사용이 권장되지 않으며, 기존 생태계의 대부분이 권한 없이 동작하도록 설계되어 있어 호환성 문제가 발생할 수 있습니다. Deno는 처음부터 보안을 전제로 설계되었기에 생태계 전체가 권한 모델과 자연스럽게 통합됩니다.
개발 시에도 최소 권한 원칙을 적용하는 것이 좋습니다.
{
"tasks": {
"dev": "deno run --watch --allow-net=0.0.0.0:3000,api.example.com --allow-read=./src,./public --allow-env=DATABASE_URL,PORT src/main.ts",
"test": "deno test --allow-read=./src,./test-fixtures --allow-net=none",
"lint": "deno lint src/",
"build": "deno compile --allow-net=0.0.0.0:3000 --allow-read=./public --output=app src/main.ts"
}
}프로덕션 환경에서는 더욱 엄격한 권한 설정이 필요합니다.
# 웹 서버: 네트워크 리슨 + 정적 파일 읽기 + 필요한 환경 변수만
deno run \
--allow-net=0.0.0.0:8080 \
--allow-read=./public,./views \
--allow-env=DATABASE_URL,JWT_SECRET,NODE_ENV \
--no-prompt \
src/server.ts프로덕션에서는 반드시 --no-prompt 플래그를 사용하세요. 프롬프트가 활성화되어 있으면 자동화된 환경에서 예상치 못한 대기 상태가 발생할 수 있습니다. 모든 필요 권한은 사전에 명시하는 것이 원칙입니다.
프로젝트의 보안을 점검할 때 다음 항목들을 확인하는 것을 권장합니다.
파일 시스템
[ ] --allow-read에 구체적인 경로가 지정되어 있는가
[ ] --allow-write에 구체적인 경로가 지정되어 있는가
[ ] 불필요한 디렉토리가 포함되어 있지 않은가
네트워크
[ ] --allow-net에 필요한 호스트만 지정되어 있는가
[ ] 포트 번호까지 제한하고 있는가
[ ] 외부 API 호출이 필요한 호스트만 허용하고 있는가
환경 변수
[ ] --allow-env에 필요한 변수만 나열되어 있는가
[ ] 민감한 변수(시크릿 키 등)가 불필요하게 노출되지 않는가
프로세스
[ ] --allow-run이 정말 필요한가
[ ] 실행 허용할 프로그램이 구체적으로 지정되어 있는가
기타
[ ] --allow-all(-A)을 사용하고 있지 않은가
[ ] --no-prompt가 CI/CD에서 활성화되어 있는가
[ ] deno.lock을 통한 의존성 무결성 검증이 활성화되어 있는가Deno의 보안 모델이 만능은 아닙니다. 인식해야 할 한계도 있습니다.
권한 범위의 한계: --allow-net=api.example.com을 허용하면 해당 호스트로의 모든 요청이 허용됩니다. 특정 경로나 HTTP 메서드 단위의 제어는 불가능합니다.
CPU/메모리 제한 부재: Deno의 권한 시스템은 I/O 접근을 제어하지만, CPU 사용량이나 메모리 할당을 제한하지는 않습니다. 무한 루프나 메모리 폭주는 별도의 방법으로 대응해야 합니다.
--allow-all 남용: 편의를 위해 --allow-all을 사용하면 보안 모델의 의미가 사라집니다. 개발 중에도 이 플래그 사용을 최소화하는 습관이 중요합니다.
// --allow-net=api.example.com 설정 시
// 다음 두 요청 모두 허용됨
await fetch("https://api.example.com/public/data"); // 의도한 접근
await fetch("https://api.example.com/admin/delete"); // 의도하지 않은 접근도 허용
// 경로 단위 제어는 애플리케이션 레벨에서 구현해야 함이러한 한계에도 불구하고, Deno의 보안 모델은 "아무 보안도 없는 것"에 비해 압도적으로 안전한 기본값을 제공합니다. 완벽한 보안은 없지만, 공격 표면(Attack Surface)을 크게 줄이는 것만으로도 상당한 가치가 있습니다.
다음 장에서는 Deno 2의 npm 호환성과 새로운 패키지 관리 방식을 살펴봅니다. 기존 Node.js 생태계와의 통합이 어떻게 이루어지는지, 그리고 JSR이라는 새로운 패키지 레지스트리가 가져올 변화를 분석합니다.
이 글이 도움이 되셨나요?
Deno 2의 npm 호환성, JSR(JavaScript Registry), import map, deno add를 통한 패키지 관리, 그리고 Node.js에서 Deno로의 마이그레이션 전략을 다룹니다.
Deno 2의 내부 아키텍처를 분석합니다. V8 엔진, Tokio 비동기 런타임, Rust 기반 구조, 단일 실행 파일 설계, 그리고 내장 도구 체인의 동작 원리를 살펴봅니다.
Deno가 채택한 웹 표준 API(fetch, WebSocket, Web Crypto, Streams)와 Deno 전용 API(Deno.serve, Deno.KV, Deno.open)를 심층적으로 분석합니다.