홈 서버가 한 대일 때는 중앙화라는 말을 굳이 생각할 일이 없다.
서버가 곧 시스템이고, 관측하는 주체와 실행하는 주체도 사실상 같은 장비에 있다.
하지만 물리 노드가 늘어나고 PaaS가 placement와 PRIMARY를 다루기 시작하면 자연스럽게 이런 구조가 떠오른다.
Control Node
├─ 모든 서버 상태 수집
├─ 모든 process 관찰
├─ 모든 보안 판단
└─ 모든 서버에 명령
보기에는 관리하기 쉽다.
나도 초기에는 비슷한 중앙 receiver 흔적을 가지고 있었다.
그런데 process lifecycle, runtime identity, resource observation, security containment까지 생각하기 시작하자 이 구조가 오히려 복잡해졌다.
그래서 PaaS / Server Ops / Security Guard의 authority를 physical node 단위로 로컬화했다.
하나의 물리 노드에는 하나의 local authority만 둔다
기본 구조는 다음과 같다.
Node A
├─ PaaS(A)
├─ Server Ops(A)
└─ Security Guard(A)
Node B
├─ PaaS(B)
├─ Server Ops(B)
└─ Security Guard(B)
그리고 가장 중요한 invariant를 정했다.
ONE PHYSICAL NODE
=
ONE LOCAL OBSERVATION / ENFORCEMENT AUTHORITY
Ops(A)는 Node A를 관찰하고 조작한다.
Guard(A)는 Node A의 observation을 해석하고 Node A에 대한 containment intent를 만든다.
Ops(B)와 Guard(B)도 마찬가지다.
노드 간 coordination이 필요하면:
PaaS(A) <-> PaaS(B)
가 담당한다.
반대로 다음은 기본 구조가 아니다.
Ops(A) -> Node B 직접 관측
Guard(A) -> Node B 직접 집행
왜 이렇게까지 나눴을까?
Process observation은 생각보다 강하게 local state에 묶인다
process를 단순히 PID 숫자로 생각하면 원격 중앙 observer도 충분해 보인다.
하지만 destructive action까지 가면 PID만으로는 부족하다.
PID는 재사용된다.
관찰 시점에는 공격 process였던 PID가 action 시점에는 다른 process를 가리킬 수도 있다.
그래서 process target은 다음처럼 stable identity를 가져야 했다.
ProcessInstanceRef
=
node_id
+ boot_id
+ pid
+ start_identity
runtime도 비슷하다.
RuntimeInstanceRef
=
node_id
+ runtime kind
+ runtime id
+ start identity
+ optional workload id
이 identity를 가장 정확하게 재검증할 수 있는 곳은 그 physical node의 local Ops다.
원격 중앙 서비스가 캐시된 상태만 보고 "아까 그 PID를 죽여"라고 명령하는 것보다, local actuator가 mutation 직전에 concrete identity를 다시 확인하는 편이 안전하다.
VALID TARGET SHAPE
+ CURRENT IDENTITY MATCH
→ action may proceed
VALID TARGET SHAPE
+ CURRENT IDENTITY CHANGED
→ reject before mutation
이런 semantics는 단순 RPC보다 local runtime ownership에 가깝다.
관측의 순서와 provenance도 로컬에서 시작한다
runtime security에서는 event가 발생했다는 사실만으로 부족하다.
어떤 observer가 어떤 순서로 봤는지, reboot를 넘었는지, observer가 재시작했는지를 구분해야 correlation이 안정적이다.
그래서 observation에는 다음 axis를 분리했다.
observer_instance_id
source_sequence
observed_monotonic_ns
observed_at_unix_ms
여기서 중요한 것은 transport가 전달한 순서와 original observer가 본 순서를 같은 것으로 취급하지 않는 것이다.
network나 queue가 끼면 전달 순서는 바뀔 수 있다.
TRANSPORT ORDER
!= SOURCE OBSERVATION ORDER
local observer가 provenance를 만들고, 그 사실이 Guard로 넘어가야 한다.
"못 봤다"와 "없었다"도 다르다
보안 시스템에서 특히 위험한 해석 중 하나는 observation이 없다는 이유로 사건이 없었다고 결론내리는 것이다.
예를 들어 kernel capability가 없거나 collector가 잠시 내려가 있었다면 network event 없음이 아니라 network observation unavailable일 수 있다.
그래서 availability와 resolution을 명시적인 상태로 뒀다.
RESOLVED
UNKNOWN
UNSUPPORTED
AVAILABLE
UNAVAILABLE
UNSUPPORTED
UNKNOWN
보지 못한 것을 안전하다고 간주하지 않는다.
이 원칙은 중앙 control tower가 모든 것을 안다는 착각을 피하는 데도 중요했다.
각 노드는 자신이 실제로 관찰 가능한 범위를 factual state로 보고한다.
Ops는 사실을 만들고 Guard는 의미를 만든다
node-local이라고 해서 모든 것을 하나의 daemon에 합친 것은 아니다.
오히려 local boundary 안에서도 역할은 더 엄격하게 나눴다.
Server Ops
= factual observation
+ capability / availability
+ bounded local actuator
+ factual result
Security Guard
= detection
+ threshold
+ correlation
+ incident
+ containment semantics
+ retry / escalation policy
예를 들어 ResourceWindow가 있다고 하자.
Ops는 CPU average, maximum interval utilization, memory basis, IO delta, window duration 같은 사실을 만들 수 있다.
CPU는 one logical core를 100 core-percent로 표현하므로 multi-core process는 100을 넘을 수도 있다.
Memory도 PROCESS_RSS, CGROUP_CURRENT, RUNTIME_REPORTED_USAGE 중 어떤 basis인지 함께 보존한다.
하지만 CPU 350% = miner라는 판단은 Ops가 하지 않는다.
그건 Guard의 security meaning이다.
Security Guard에도 arbitrary shell 권한을 주지 않는다
분산 구조에서 중앙 권한을 없앴다고 local Guard에 root shell을 다 주면 문제는 그대로다.
그래서 Guard가 보낼 수 있는 action도 typed containment intent로 제한했다.
초기 boundary는 다음에 가깝다.
TERMINATE_PROCESS
STOP_WORKLOAD
QUARANTINE_WORKLOAD
그리고 action / operation / target shape 조합이 유효하지 않으면 mutation 전에 거부한다.
예를 들어 process terminate에 workload runtime target을 넣거나, terminate action에 revoke operation을 붙이는 식의 요청은 실행 실패가 아니다.
애초에 지원하지 않는 request shape다.
invalid action/operation/target combination
→ REJECTED
→ ACTION_OPERATION_UNSUPPORTED
→ action_attempted = false
반면 request shape는 정상이지만 실행 직전 stable identity가 바뀌었다면 의미가 다르다.
valid target shape
+ current concrete identity mismatch
→ TARGET_IDENTITY_MISMATCH
이 두 실패를 같은 EXECUTION_FAILED로 뭉개지 않았다.
왜 실행하지 않았는지가 security automation에서는 중요하기 때문이다.
Quarantine은 "멈췄다"보다 긴 상태다
workload quarantine을 단순 stop action으로 보면 PaaS가 바로 다시 띄울 수 있다.
그래서 quarantine은 runtime mutation과 logical hold를 분리했다.
APPLY
→ exact runtime revalidate
→ runtime non-running 확인
→ workload hold ACTIVE
해제도 조심해야 한다.
REVOKE / expiry
→ hold release
hold release
!= automatic start
!= automatic restart
!= automatic reschedule
보안 정책이 끝났다는 사실과 서비스를 다시 시작해도 된다는 운영 판단은 같은 것이 아니다.
PaaS는 이 hold의 사실을 lifecycle에 반영하지만 Guard의 rule을 다시 해석하지 않는다.
Cross-node에는 raw event보다 필요한 상태만 보낸다
node-local 구조라고 해서 cluster 전체에서 아무것도 공유하지 않는 것은 아니다.
PaaS mesh는 여전히 필요하다.
다만 공유의 단위를 구분했다.
kernel/process raw event
→ local Ops
→ local Guard
→ local security/runtime state
→ local PaaS
→ mesh projection when needed
모든 syscall이나 process event를 다른 노드로 broadcast하는 것은 기본 목표가 아니다.
다른 PaaS node가 알아야 하는 것은 보통 이 workload가 security hold 상태인가, 현재 node에서 runtime이 available한가, placement/promotion이 허용되는가 같은 product-level state다.
raw evidence의 authority는 해당 physical node에 남긴다.
하드웨어가 바뀌어도 authority 모델은 그대로다
node-local을 특정 장비에 묶는 구조로 생각할 수도 있다.
하지만 physical hardware는 product identity가 아니다.
서버가 교체되면 새 하드웨어에 PaaS/Ops/Guard stack을 bootstrap하고 node identity를 부여한 뒤 mesh에 참여시키면 된다.
old physical node removed
→ new hardware
→ local stack bootstrap
→ node enrollment
→ mesh participation
→ workload placement
다른 노드의 Ops가 새 장비를 영구적으로 대신 관찰하게 만드는 것이 아니다.
이 덕분에 "현재 어느 장비가 control tower인가"라는 질문도 핵심 architecture에서 빠진다.
중앙집중을 줄인 이유는 분산 시스템을 멋있게 만들기 위해서가 아니다
이 구조를 택한 이유는 분산 자체가 목적이어서가 아니다.
runtime security에서 가장 위험한 것은 오래된 상태를 authoritative fact처럼 사용하는 것이다.
process는 짧게 살고 죽는다. container도 교체된다. reboot가 일어나면 PID의 의미도 바뀐다. security action은 실제 mutation을 일으킨다.
그래서 authority를 runtime에 가장 가까운 곳에 둔 것이다.
LOCAL FACTS
→ LOCAL SECURITY DECISION
→ LOCAL BOUNDED ACTION
CROSS-NODE
→ PAAS COORDINATION
이 구분을 해두면 노드 수가 늘어나도 보안 subsystem이 거대한 원격 shell controller로 변하지 않는다.
그리고 어떤 노드가 잠시 network partition 상태가 되더라도 그 노드의 local observation과 local containment capability 자체는 다른 control tower 연결 여부에 종속되지 않는다.
분산 구조를 선택한 것이 아니라 runtime truth의 위치를 따라 authority를 배치한 것에 가깝다.