Skip to content

pasu — 허용된 흐름만 통과시키는 관문

pasu  把守

AI 에이전트를 위한 셀프호스티드 보안 가드 — 온프렘·망분리(air-gapped)·규제 환경을 위해 만들었습니다.
트레이싱 → 도구 호출 가드 → 커널 강제 egress, 이 3계층을 Linux 호스트 한 대에서. 쿠버네티스도 클라우드도 없이, 네트워크 밖으로 나가는 데이터도 없이.

CI License: Apache-2.0 Rust Platform: Linux first Self-hosted / air-gapped No Kubernetes required

English

에이전트를 믿지 않고도 egress를 통제합니다. 협조적 가드는 에이전트가 선언한 호출만 볼 수 있어서, 도구가 소켓을 직접 열면 그대로 빠져나갑니다. pasu는 그 아래에 에이전트가 우회할 수 없는 커널 eBPF 가드를 두고, 모든 판정을 감사 로그로 남깁니다. 전부 호스트 안에서 돌아가고, 밖으로 보내는 데이터는 없습니다. enforcing > cooperative.


pasu가 필요한 이유

AI 에이전트는 프롬프트 인젝션(prompt injection)에 취약하고, 장악당한 에이전트는 데이터가 빠져나가는 통로가 됩니다. 온프렘·망분리·규제 환경이라면 제약이 둘 더 붙습니다 — 에이전트 트래픽을 클라우드/SaaS 가드로 내보낼 수 없고, 우회당하기 쉬운 단일 검사 대신 다층 방어와 감사 증적이 필요합니다.

pasu는 이 제약 안에서 동작하도록 만들었습니다. Linux 호스트 한 대에서 전부 돌아가고, 쿠버네티스도 외부 서비스도 필요 없으며, 정책 하나로 세 계층을 함께 적용합니다.

pasu 계층 egress 방어: 정책 하나가 협조적 프록시와 강제적 커널 eBPF 가드를 구동; 프록시를 우회한 egress도 커널이 drop, 모든 결정은 감사 기록

  • ① 트레이싱 / 감사 (pasu-audit) — 모든 판정을 기록합니다. 파일·SIEM으로는 JSONL, 관측 스택으로는 OpenTelemetry(OTLP) 스팬으로. 데이터를 밖으로 내보내지 않고도 감사 증적이 남습니다.
  • ② 도구 호출 가드 (협조적) (pasu-proxy LLM-API 프록시) — 에이전트의 base_url을 프록시로 향하게 하면, provider 응답의 도구 호출을 파싱해 검사하고 필요하면 사람이 승인(HITL)합니다. 프레임워크를 가리지 않습니다 — 어떤 SDK든 base_url만 바꾸면 됩니다. 도구 이름과 인자까지 보는 계층이지만, 단독으로는 우회될 수 있습니다.
  • ③ egress 강제 (커널) (pasu-egress / pasu-ebpf) — 커널 cgroup egress를 기본 차단합니다. 언어를 가리지 않고 우회도 불가능해서, ②를 빠져나간 트래픽까지 여기서 최종 차단됩니다.

핵심은 이 강제 계층입니다: 도구가 ②를 우회해 자체 reqwest로 나가더라도 커널이 egress를 drop합니다 — 선언된 것만 보는 협조적 가드로는 못 막는 지점입니다.

온프렘·규제 환경 적합성

도구 호출 가드 · 커널 egress · 감사, 이 세 계층을 셀프호스티드 서버 한 대에서 함께 제공합니다.

온프렘 AI 에이전트 pasu 적용 전후: 적용 전에는 에이전트의 도구 호출·egress를 걸러줄 가드 지점이 없어(클라우드/SaaS 가드는 망분리에서 못 쓰고 방화벽은 all-or-nothing) 유출·오작동을 세밀하게 못 막는다. 적용 후에는 한 호스트에서 pasu-proxy(② 도구 가드)와 eBPF 커널 egress(③ 강제·기본 차단)와 감사(①)를 함께 제공해, 허용된 내부 LLM만 통과하고 프록시를 우회한 egress는 커널이 drop한다

  • 쿠버네티스도 클라우드도 필요 없습니다. Linux 호스트 하나면 되고, pasu run으로 어떤 에이전트든 감쌉니다.
  • 망분리 환경에서 동작합니다. 런타임에 외부로 연결하지 않습니다. 텔레메트리 전송은 선택이고, 전송 대상도 자체 collector입니다.
  • 커널 인라인 egress + 에이전트 의도 + 감사를 한 호스트에서 함께 제공합니다.
  • Apache-2.0, 감사 가능한 Rust로 작성했고, 모든 crate는 trait 뒤에 있어 교체할 수 있습니다.

한계

  • Linux 전용 — eBPF 커널 강제는 Linux에서만 동작합니다. macOS·Windows는 협조 계층(LLM-API 프록시)만 지원하며 커널 egress 강제가 없습니다.
  • 커널 권한 필요 — eBPF attach에 root 또는 CAP_BPF가 필요합니다(프록시 계층은 무권한).
  • 프록시 계층 단독 우회 가능 — 도구가 자체 소켓으로 직접 연결하면 프록시가 보지 못합니다. 이 경우는 커널 계층이 차단합니다.
  • 커널 계층은 L3/L4 — IP·도메인 단위이며 TLS 페이로드는 볼 수 없습니다. 허용된 도메인으로의 유출은 allowlist로 막지 못합니다. 그 경로의 내용 검사는 프록시 계층이 맡습니다(아래).
  • 내용 검사는 프록시가 아는 요청 형태에서만Inspector는 provider별로 파싱한 프롬프트 텍스트를 봅니다. 파싱되지 않는 본문은 그대로 통과하며, 도구가 프록시를 우회해 직접 보내는 트래픽은 커널 계층이 주소로만 막습니다.
  • 탐지는 정규식·체크섬 — NER 모델을 돌리지 않습니다. 메시지마다 실행되는 경로라 감당할 수 없고, 그래서 사람 이름처럼 패턴이 없는 PII는 잡지 못합니다.
  • 마스킹은 단방향 — 값을 자리표시자로 바꿔 보내며 원본을 어디에도 남기지 않습니다. 되돌릴 수 없고, 그래서 프록시가 PII 저장소가 되지 않습니다. 응답에서 원값을 복원하는 방식은 보안 태세가 다른 별개 결정입니다.
  • 응답 마스킹 없음 — 모델이 생성한 PII는 다루지 않습니다. 요청 경로만입니다.
  • 스트리밍은 버퍼링 후 전달 — SSE 응답의 도구 호출도 재조립해 검사하지만, 전체 스트림을 받아 검사한 뒤 한 번에 전달합니다(증분 릴레이 없음). DNS 인식은 best-effort입니다.
  • 입력 계층 방어 아님 — 프롬프트 인젝션이나 모델 오작동은 다루지 않습니다. pasu는 egress와 도구 의도에 대한 최종 방어선입니다.
  • 초기 단계(MVP) — 보안 인증과 프로덕션 도입 사례가 없습니다.

정책 (Falco에서 영향받은 YAML)

rules:
  - name: allow-llm
    match: { host: ".openai.com" }   # 도메인 + 서브도메인
    action: allow
  - name: confirm-transfer
    match: { tool: transfer_funds }
    action: ask                      # 사람 승인(HITL)
default: deny                        # fail-closed

빠른 시작

어떤 에이전트든 감싸기 — 코드 수정 없이

pasu는 에이전트가 아니라 가드입니다. 어떤 프레임워크를 쓰는지는 상관없습니다. pasu run은 명령을 전용 cgroup에 넣고, 그 명령이 첫 동작을 하기 전에 커널 가드를 걸어 둡니다.

sudo pasu run --policy rules.yaml -- python crew.py        # CrewAI / LangChain / 무엇이든
sudo pasu run --policy rules.yaml -- npx some-agent "task"  # 언어 무관

정책에 없는 건 전부 커널이 drop합니다. 에이전트나 인젝션당한 도구가 소켓을 직접 열어도 마찬가지입니다.

어떤 SDK든 도구 호출 가드 — LLM-API 프록시

에이전트의 base_urlpasu-proxy로 향하게 하세요. pasu-proxy는 실제 provider로 요청을 넘기면서 모델이 돌려준 도구 호출을 파싱하고, 정책이 거부한 호출은 에이전트가 실행하기 전에 막습니다(fail-closed). 도구 호출 결정은 provider 응답에 실려 오기 때문에, provider 포맷만 파싱하면 어떤 SDK든 커버됩니다 — 프레임워크별 어댑터가 필요 없습니다.

use pasu_core::Guard;
use pasu_proxy::{router, Provider, ProxyState};
use pasu_rules::RulesetEngine;
use std::sync::Arc;

let state = Arc::new(ProxyState {
    guard: Guard::new(RulesetEngine::from_yaml(policy_yaml)?, "llm-proxy"),
    client: reqwest::Client::new(),
    upstream_base: "https://api.openai.com".into(),
    provider: Provider::OpenAi,
});
let app = router(state);   // axum Router — 서빙한 뒤 에이전트 base_url을 여기로

OpenAI 호환·Anthropic·Gemini를 지원합니다(세 포맷이 사실상 모든 SDK를 커버). 논스트리밍과 스트리밍(SSE) 응답 모두 검사합니다 — SSE는 delta 조각들을 재조립해 같은 정책으로 판정하며, 전체 스트림을 버퍼링한 뒤 전달합니다.

Ask 판정에 사람 승인을 붙이려면 바이너리를 --ui <주소>로 실행하세요 — 승인 대기열 웹 UI(/)와 결정 감사 뷰(/audit)가 함께 뜹니다. 미지정 시 Ask는 fail-closed로 거부됩니다.

사이드카로 배포할 수도 있습니다 — 슬림하고 권한이 필요 없는 이미지(deploy/proxy/Dockerfile)와 에이전트+프록시를 한 파드에 담은 예시(deploy/proxy/k8s-sidecar.yaml, 에이전트 base_urllocalhost).

pasu-proxy 사이드카 배치: 한 파드에 에이전트와 무권한 pasu-proxy를 두고 base_url만 localhost로 바꾼다. 프록시가 provider로 포워딩하며 응답의 도구 호출을 검사(deny 403 · ask HITL)하고 stderr JSONL 감사를 남긴다. 노드의 pasu-egress(privileged)가 선택적 심층방어로, 프록시를 우회한 직접 소켓 트래픽을 커널에서 drop한다

가장 간단한 부착은 이렇습니다 — 프록시 컨테이너 하나 띄우고, 에이전트 쪽은 환경변수 하나만 바꿉니다:

docker build -f deploy/proxy/Dockerfile -t pasu-proxy .
docker run -d -p 8788:8788 -v "$PWD/rules.yaml:/etc/pasu/rules.yaml:ro" pasu-proxy
export OPENAI_BASE_URL=http://127.0.0.1:8788/v1   # 에이전트는 이것만 (OpenAI SDK 계열)

기본 CMD가 /etc/pasu/rules.yaml 정책으로 api.openai.com에 포워딩합니다. 내부 vLLM 등 다른 upstream이면 인자를 덧붙이면 됩니다(--upstream http://vllm:8000 --provider openai). 컨테이너 없이 직접 실행도 됩니다:

pasu-proxy --policy rules.yaml --listen 127.0.0.1:8788 --upstream https://api.openai.com

더 깊게: 커널 egress 가드 (Linux)

Linux 커널 egress 가드 — 같은 YAML을 커널 allowlist로 변환합니다 (전용 cgroup을 쓰고, 루트 cgroup은 절대 금지):

sudo pasu-daemon --policy rules.yaml --cgroup-path /sys/fs/cgroup/my-agent
# 정책 파일 없이 플래그/TOML로 직접 지정하려면:
sudo pasu-egress --cgroup-path /sys/fs/cgroup/my-agent --allow-domain api.openai.com

IPv4/IPv6 리터럴 allow는 정적 항목이 되고, 정확한 호스트명은 주기적으로 재해석됩니다(양 계열). 접미 패턴(.openai.com)은 아직 커널로 내려가지 못해 리포트만 남깁니다 — DNS 응답 스니핑이 들어오면 해결됩니다. 커널은 v4·v6 모두 기본 차단이라, 변환 결과는 정책보다 좁아질 뿐 넓어지지 않습니다.

--admin-socket /run/pasu.sock을 붙이면 재시작 없이 실행 중인 가드를 들여다보고 수정할 수 있습니다 (UI도 이 소켓을 씁니다).

echo status        | socat - UNIX-CONNECT:/run/pasu.sock   # {"cgroup_path":…,"allow_ips":[…]}
echo 'allow 1.2.3.4' | socat - UNIX-CONNECT:/run/pasu.sock  # 지금 커널 allowlist에 추가
echo 'deny 1.2.3.4'  | socat - UNIX-CONNECT:/run/pasu.sock  # 지금 제거

웹 UI — 승인(/), 감사(/audit), 그리고 실시간 egress 대시보드(/egress: 커널 필터 커버리지, allowlist 추가·삭제, 룰별 verdict와 도구 가드를 보여주는 읽기 전용 뷰):

use pasu_ui::dashboard::{EgressAdmin, EgressUi};
let egress = EgressUi::new(EgressAdmin::new("/run/pasu.sock"), Some("rules.yaml".into()));
pasu_ui::serve_all(addr, approvals, feed, Some(egress)).await?;   // + /egress

커널 없이 둘러보기 (mock 가드 소켓):

cargo run -p pasu-ui --example ui_demo   # http://127.0.0.1:8787/egress

요청 검사 (PII·시크릿)

프록시는 응답의 도구 호출만 보던 계층이었습니다. 지금은 요청이 나가기 전에도 봅니다. 그 자리여야 하는 이유는 하나입니다 — 커널 계층은 provider 주소를 허용해야 에이전트가 동작하고, 그 뒤는 TLS라 내용이 보이지 않습니다. 요청 본문이 내용을 읽을 수 있는 유일한 지점입니다.

# 내장 한국 PII 규칙으로 검사
pasu-proxy --policy policy.yaml --upstream https://api.openai.com --block-pii-kr

# 이미 갖고 있는 Presidio recognizer YAML로 검사
pasu-proxy --policy policy.yaml --upstream https://api.openai.com \
  --presidio-rules ./recognizers.yaml --presidio-min-score 0.5

기본은 꺼져 있습니다. 이 패턴에 노출이 없는 배포에서 오탐으로 에이전트가 멈추는 편이 더 나쁩니다.

차단 대신 가리기

걸린 값만 지우고 나머지는 보냅니다. 지원 티켓을 요약하는 에이전트에게 필요한 건 티켓이 막히는 것이 아니라 고객 번호가 빠진 티켓입니다.

# 걸리면 가리고 보낸다
pasu-proxy … --block-pii-kr --redact

# 대부분은 가리되, 주민번호는 무조건 막는다
pasu-proxy … --block-pii-kr --redact --block-rule ko-rrn

차단이 기본입니다. 아무도 판단하지 않은 규칙에 대해 안전한 답은 멈추는 쪽입니다. --block-rule--redact-rule이 같은 규칙을 가리키면 차단이 이깁니다 — 보안 정책에서 모순은 엄격한 쪽으로 읽는 것이 맞습니다.

치환은 단방향입니다. 원본을 어디에도 두지 않으므로 되돌릴 수 없고, 그래서 프록시가 자신이 막으려던 데이터의 저장소가 되지 않습니다. 자리표시자 [REDACTED:ko-rrn]은 길이도 모양도 보존하지 않습니다 — 길이를 맞춘 마스크는 가린 값을 흘립니다.

붙이는 방식

검사기는 레이어를 고치지 않고 붙습니다. pasu-core의 trait 하나입니다.

pub trait Inspector: Send + Sync {
    fn name(&self) -> &str;
    fn inspect(&self, text: &str) -> Vec<Finding>;
}

판정이 아니라 finding을 냅니다. 무엇을 어디서 찾았는지만 말하고, 그게 차단인지 마스킹인지는 레이어가 정합니다. Allow/Deny로 답했다면 마스킹에 필요한 span을 이미 버린 뒤가 됩니다.

Finding매치된 값을 싣지 않습니다. 규칙 id와 위치뿐입니다. 잡은 값을 인용하는 차단 메시지는 그 자체가 막으려던 유출입니다.

Presidio 룰 가져오기

교환 표준은 없습니다. Presidio YAML도 Presidio 것이지 규격이 아닙니다. 그래서 새 포맷을 만들지 않고 사람들이 이미 export한 것을 읽습니다.

넘어오지 못하는 것이 넷이고, 전부 이름과 이유를 붙여 보고합니다.

score Presidio는 0.01짜리 약한 패턴도 배포합니다(context가 점수를 올려주니까). 여기엔 점수가 없고 finding 하나면 차단이라, 임계값 없이 가져오면 평범한 문장에 걸립니다
context words 올릴 수단이 없어 임포트된 패턴은 Presidio에서보다 오탐이 잦습니다
정규식 방언 Python re → Rust regex. 백트래킹이 없어 ReDoS가 없는 대신 lookaround·backreference는 컴파일되지 않습니다
체크섬 Python 검증 로직은 넘어오지 않습니다

로딩은 fail-closed입니다. 못 읽는 recognizer가 하나라도 있으면 에러입니다. 절반만 읽고 성공을 보고하면 운영자가 "커버된다"고 믿게 되고, 그건 거부당하는 것보다 나쁩니다.

컨테이너로 실행

커널 가드는 다른 eBPF 도구와 똑같이 컨테이너로 돌아갑니다 — CAP_BPF + CAP_NET_ADMIN과 cgroup v2 마운트만 있으면 됩니다. 빠르게 확인해 보기(1.1.1.1만 허용하고, 앱이 뭘 하든 나머지는 커널이 drop):

docker build -f deploy/Dockerfile -t pasu-egress:latest .
./deploy/demo.sh    # allowed -> reachable · blocked -> dropped · RESULT: PASS

사이드카(deploy/docker-compose.yml)와 쿠버네티스(deploy/k8s/) 배치, cgroup 타겟팅 규칙은 **docs/deployment.md**에 정리해 두었습니다.

크레이트

pasu 크레이트 지도: 모든 크레이트는 pasu-core에만 의존

crate 역할
pasu-core 공유 타입(Event / Verdict / Finding)과 trait(RuleEngine · Layer · Approver · AuditSink · Inspector), 그리고 Guard 파사드
pasu-rules RuleEngine — Falco에서 영향받은 YAML 룰셋(allow/deny/ask, 기본 fail-closed)
pasu-proxy LLM-API 리버스 프록시 — 응답의 도구 호출과 요청의 프롬프트를 같은 Guard로 가드; 프레임워크 무관(base_url만). wire format은 WireFormat trait이라 사내 자체 포맷도 포크 없이 꽂힙니다
pasu-ui 경량 웹 UI — HITL 승인(/)과 감사·egress 대시보드(/audit, /egress)
pasu-audit 감사 sink — JSONL(stderr/파일/SIEM), 인메모리, OpenTelemetry(OTLP 스팬, otel feature)
pasu-egress · pasu-ebpf · pasu-ebpf-common 커널 eBPF cgroup egress — 기본 차단 allowlist, DNS 인식 (Linux)
pasu-daemon composition root — 정책 YAML을 커널 가드로 변환(정책 하나로 양 계층)
pasu-cli pasu 명령 — pasu run으로 어떤 에이전트든 가드된 cgroup에 감쌈
pasu-pii-kr 한국 PII 탐지(정규식 + 주민번호·사업자번호·Luhn 체크섬). 다른 pasu 크레이트에 의존하지 않아 pasu를 모르는 게이트웨이도 쓸 수 있습니다
pasu-inspect-pii-kr · pasu-inspect-presidio 어댑터. 스캐너를 Inspector로 이어 붙입니다. 프록시는 이 둘을 feature로만 당겨쓰므로, 원하지 않으면 컴파일되지 않습니다

모든 crate는 pasu-core에만 의존합니다(순환 없음). 룰 포맷과 프레임워크 통합은 trait 뒤에 있어 교체할 수 있습니다.

어느 이음매에 무엇을 구현하면 되는지는 docs/adapting.md에 있습니다.

의존성

재현성을 위해 핵심 의존성의 버전을 고정해 두었습니다.

의존성 버전 라이선스 이유
aya (+ aya-log, aya-build) git 773ca715 MIT / Apache-2.0 aya 다음 릴리스 전까지 고정 — 고정하지 않은 git 의존이 upstream API 변경으로 CI를 깨뜨린 적 있음
Falco 의존성이 아님 — 룰 포맷 아이디어만 빌렸을 뿐 Falco 코드는 없음

지표

pasu 실측 오버헤드와 검증 지도: criterion 실측(Apple M4)으로 정책 판정 0.06µs·응답 파싱 0.7µs·SSE 재조립 4.5µs — 로그 스케일에서 통상 LLM 왕복(~1초)보다 5~7자리수 작음. 주장↔검증 매트릭스: 커널 default-deny drop·런타임 allow/deny·도메인 allowlist는 CI의 실제 커널 E2E로, 거부 tool call 403·SSE 가드는 유닛+와이어 E2E로, ask fail-closed는 유닛으로, pasu run 래핑은 실제 커널 E2E로 검증. 총 82개 테스트, CI 4잡

  • 응답당 가드 비용 (criterion 실측, Apple M4): 정책 판정 ~0.06 µs · 응답 파싱 ~0.7 µs · SSE 재조립(40청크) ~4.5 µs. 재현: cargo bench -p pasu-rules -p pasu-proxy
  • 테스트 82개: 포터블 62(core·rules·ui·audit·proxy, 와이어 E2E 포함) + Linux 20(유닛 14 · 실제 커널 E2E 6 — egress 4 + pasu run 2)
  • CI 4개 잡이 push마다 실행 — fmt·clippy·test(stable) · eBPF build+unit(nightly + bpf-linker) · eBPF E2E(privileged, 실제 Ubuntu 커널) · cargo-deny(취약점/라이선스/소스)
  • crate 10개, 순환 없는 코어 하나 (모든 crate가 pasu-core에만 의존)

상태

MVP — 엔진, 정책, HITL, 감사, 배포, 벤치마크까지 갖췄습니다.

기능 crate 상태
커널 기본 차단 allowlist (DNS 인식) egress/ebpf
정책 언어 (YAML) rules
LLM-API 프록시 — 도구 호출 가드 · HITL (어떤 SDK든) proxy ✅ OpenAI · Anthropic · Gemini · SSE 재조립
승인 + 감사 UI ui
감사 sink (JSONL / OTLP) audit
config 기반 daemon + systemd egress + packaging
정책 파일 하나 → 양 계층 daemon

다음 계획: SSE 증분 릴레이(현재는 전체 버퍼링), eBPF로 LLM 트래픽을 프록시로 강제 경유; 정밀 DNS 응답 스니핑(toFQDN — 커널에서 접미 호스트까지 처리), eBPF 계층 감사 emit, 컨트롤 플레인 API와 더 나은 UI, 그리고 crates.io 릴리스(현재 aya는 git 고정).

개발

cargo test              # 포터블 크레이트: core, rules, ui, audit, proxy (stable)
cargo build -p pasu-egress   # eBPF 스택 — Linux 전용, nightly + bpf-linker

플랫폼

Linux 우선, 셀프호스티드·망분리 친화적입니다. eBPF 커널 강제는 Linux 전용이고, 호스트 한 대에서 쿠버네티스나 런타임 외부 연결 없이 돌아갑니다. 텔레메트리 전송(OTLP/JSONL)은 선택이고 전송 대상도 자체 collector입니다. macOS·Windows에서는 개발용으로 LLM-API 프록시와 UI(협조적 계층)만 쓸 수 있고, 커널 강제는 없습니다.

기여

기여 환영합니다 — CONTRIBUTING.md를 참고하세요. 요약하면: Conventional Commits, DCO 서명(git commit -s), 피처 브랜치 → PR → CI 통과.

보안

pasu는 커널에서 동작하는 보안 도구입니다. 취약점은 공개 이슈 대신 비공개로 제보해 주세요 — SECURITY.md.

감사의 글

  • 정책 문법은 Falco의 룰 포맷에서 아이디어를 얻었습니다. pasu는 Falco 프로젝트나 CNCF와 제휴하거나 보증받은 관계가 아닙니다.

라이선스

Apache-2.0.

About

Don't trust your agent. Jail it. — eBPF kernel egress guard + LLM-API proxy for AI agents

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages