요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
대안 비교
SafeSetID.rst:47-96Userspace helper, user namespace와 기존 LSM 접근의 한계를 비교합니다.
Policy 설정
SafeSetID.rst:97-118Securityfs allowlist 형식과 현재 `setgroups()` 제한 상태를 설명합니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
=========
SafeSetID
=========
SafeSetID is an LSM module that gates the setid family of syscalls to restrict
UID/GID transitions from a given UID/GID to only those approved by a
system-wide allowlist. These restrictions also prohibit the given UIDs/GIDs
from obtaining auxiliary privileges associated with CAP_SET{U/G}ID, such as
allowing a user to set up user namespace UID/GID mappings.
Background
==========
In absence of file capabilities, processes spawned on a Linux system that need
to switch to a different user must be spawned with CAP_SETUID privileges.
CAP_SETUID is granted to programs running as root or those running as a non-root
user that have been explicitly given the CAP_SETUID runtime capability. It is
often preferable to use Linux runtime capabilities rather than file
capabilities, since using file capabilities to run a program with elevated
privileges opens up possible security holes since any user with access to the
file can exec() that program to gain the elevated privileges.
While it is possible to implement a tree of processes by giving full
CAP_SET{U/G}ID capabilities, this is often at odds with the goals of running a
tree of processes under non-root user(s) in the first place. Specifically,
since CAP_SETUID allows changing to any user on the system, including the root
user, it is an overpowered capability for what is needed in this scenario,
especially since programs often only call setuid() to drop privileges to a
lesser-privileged user -- not elevate privileges. Unfortunately, there is no
generally feasible way in Linux to restrict the potential UIDs that a user can
switch to through setuid() beyond allowing a switch to any user on the system.
This SafeSetID LSM seeks to provide a solution for restricting setid
capabilities in such a way.
The main use case for this LSM is to allow a non-root program to transition to
other untrusted uids without full blown CAP_SETUID capabilities. The non-root
program would still need CAP_SETUID to do any kind of transition, but the
additional restrictions imposed by this LSM would mean it is a "safer" version
of CAP_SETUID since the non-root program cannot take advantage of CAP_SETUID to
do any unapproved actions (e.g. setuid to uid 0 or create/enter new user
namespace). The higher level goal is to allow for uid-based sandboxing of system
services without having to give out CAP_SETUID all over the place just so that
non-root programs can drop to even-lesser-privileged uids. This is especially
relevant when one non-root daemon on the system should be allowed to spawn other
processes as different uids, but it's undesirable to give the daemon a
basically-root-equivalent CAP_SETUID.
Other Approaches Considered
===========================
Solve this problem in userspace
-------------------------------
For candidate applications that would like to have restricted setid capabilities
as implemented in this LSM, an alternative option would be to simply take away
setid capabilities from the application completely and refactor the process
spawning semantics in the application (e.g. by using a privileged helper program
to do process spawning and UID/GID transitions). Unfortunately, there are a
number of semantics around process spawning that would be affected by this, such
as fork() calls where the program doesn't immediately call exec() after the
fork(), parent processes specifying custom environment variables or command line
args for spawned child processes, or inheritance of file handles across a
fork()/exec(). Because of this, as solution that uses a privileged helper in
userspace would likely be less appealing to incorporate into existing projects
that rely on certain process-spawning semantics in Linux.
Use user namespaces
-------------------
Another possible approach would be to run a given process tree in its own user
namespace and give programs in the tree setid capabilities. In this way,
programs in the tree could change to any desired UID/GID in the context of their
own user namespace, and only approved UIDs/GIDs could be mapped back to the
initial system user namespace, affectively preventing privilege escalation.
Unfortunately, it is not generally feasible to use user namespaces in isolation,
without pairing them with other namespace types, which is not always an option.
Linux checks for capabilities based off of the user namespace that "owns" some
entity. For example, Linux has the notion that network namespaces are owned by
the user namespace in which they were created. A consequence of this is that
capability checks for access to a given network namespace are done by checking
whether a task has the given capability in the context of the user namespace
that owns the network namespace -- not necessarily the user namespace under
which the given task runs. Therefore spawning a process in a new user namespace
effectively prevents it from accessing the network namespace owned by the
initial namespace. This is a deal-breaker for any application that expects to
retain the CAP_NET_ADMIN capability for the purpose of adjusting network
configurations. Using user namespaces in isolation causes problems regarding
other system interactions, including use of pid namespaces and device creation.
Use an existing LSM
-------------------
None of the other in-tree LSMs have the capability to gate setid transitions, or
even employ the security_task_fix_setuid hook at all. SELinux says of that hook:
"Since setuid only affects the current process, and since the SELinux controls
are not based on the Linux identity attributes, SELinux does not need to control
this operation."
Directions for use
==================
This LSM hooks the setid syscalls to make sure transitions are allowed if an
applicable restriction policy is in place. Policies are configured through
securityfs by writing to the safesetid/uid_allowlist_policy and
safesetid/gid_allowlist_policy files at the location where securityfs is
mounted. The format for adding a policy is '<UID>:<UID>' or '<GID>:<GID>',
using literal numbers, and ending with a newline character such as '123:456\n'.
Writing an empty string "" will flush the policy. Again, configuring a policy
for a UID/GID will prevent that UID/GID from obtaining auxiliary setid
privileges, such as allowing a user to set up user namespace UID/GID mappings.
Note on GID policies and setgroups()
====================================
In v5.9 we are adding support for limiting CAP_SETGID privileges as was done
previously for CAP_SETUID. However, for compatibility with common sandboxing
related code conventions in userspace, we currently allow arbitrary
setgroups() calls for processes with CAP_SETGID restrictions. Until we add
support in a future release for restricting setgroups() calls, these GID
policies add no meaningful security. setgroups() restrictions will be enforced
once we have the policy checking code in place, which will rely on GID policy
configuration code added in v5.9.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
SafeSetID
1-9SafeSetID는 setid syscall family를 통제해 특정 UID/GID에서 system-wide allowlist가 승인한 UID/GID로만 전환할 수 있게 하는 LSM module입니다.
이 제한은 user namespace UID/GID mapping 설정처럼 `CAP_SETUID` 또는 `CAP_SETGID`와 연관된 보조 권한을 해당 UID/GID가 얻는 것도 금지합니다.
배경
10-32File capability가 없을 때 다른 user로 전환해야 하는 Linux process는 `CAP_SETUID` 권한으로 실행되어야 합니다. 이 권한은 root program 또는 명시적으로 `CAP_SETUID` runtime capability를 받은 non-root program에 부여됩니다.
Elevated privilege로 program을 실행하는 file capability는 file에 접근할 수 있는 모든 user가 `exec()`로 그 권한을 얻을 수 있어 보안 구멍을 만들 수 있으므로 Linux runtime capability가 더 나은 경우가 많습니다.
Process tree에 전체 `CAP_SETUID` 또는 `CAP_SETGID` capability를 주는 것은 non-root user로 process tree를 실행하려는 목적과 충돌합니다. 특히 `CAP_SETUID`는 root를 포함한 시스템의 어떤 user로든 바꿀 수 있어, 대개 `setuid()`로 더 낮은 권한을 내리기만 하는 scenario에는 지나치게 강합니다.
Linux에는 `setuid()`로 전환할 수 있는 UID를 제한하면서 시스템의 모든 user로 전환하는 권한을 부여하지 않는 일반적이고 실용적인 방법이 없습니다. SafeSetID는 이 setid capability를 제한하려는 해결책입니다.
주요 사용 사례
33-46주요 사용 사례는 non-root program이 완전한 `CAP_SETUID` 권한 없이 다른 신뢰하지 않는 UID로 전환하게 하는 것입니다. 전환 자체에는 여전히 `CAP_SETUID`가 필요하지만 SafeSetID 제약으로 UID 0으로 `setuid`하거나 새 user namespace를 만들고 들어가는 승인되지 않은 동작을 할 수 없습니다.
상위 목표는 non-root program이 더 낮은 권한의 UID로 내려가기 위해 곳곳에 사실상 root와 같은 `CAP_SETUID`를 배포하지 않고도 system service를 UID 기반으로 sandbox하는 것입니다. 한 non-root daemon이 다른 UID로 process를 spawn해야 하지만 daemon에 강력한 `CAP_SETUID`를 주고 싶지 않을 때 특히 유용합니다.
Userspace helper 대안
47-65한 대안은 application에서 setid capability를 완전히 제거하고 privileged helper가 process spawn과 UID/GID 전환을 담당하도록 semantics를 refactor하는 것입니다.
하지만 `fork()` 뒤 즉시 `exec()`하지 않는 호출, parent가 child의 environment variable이나 command-line argument를 지정하는 경우, `fork()`/`exec()`를 통한 file handle 상속 등 많은 process spawning semantics가 영향을 받습니다.
따라서 privileged userspace helper 방식은 특정 Linux process-spawning semantics에 의존하는 기존 project에 통합하기가 덜 매력적일 가능성이 큽니다.
User namespace 대안
66-87또 다른 대안은 process tree를 자체 user namespace에서 실행하고 tree의 program에 setid capability를 주는 것입니다. 자체 namespace에서는 원하는 UID/GID로 바꾸되 승인한 UID/GID만 initial system user namespace로 mapping하여 privilege escalation을 막을 수 있습니다.
그러나 user namespace를 다른 namespace type과 조합하지 않고 단독으로 쓰는 것은 일반적으로 실용적이지 않으며 조합 자체가 항상 가능한 것도 아닙니다.
Linux capability 검사는 entity를 `소유`하는 user namespace를 기준으로 합니다. 예를 들어 network namespace 접근 권한은 task가 실행되는 user namespace가 아니라 network namespace를 소유한 user namespace context에서 task의 capability를 검사합니다.
따라서 새 user namespace에서 process를 spawn하면 initial namespace가 소유한 network namespace에 접근하지 못하게 됩니다. Network configuration을 조정하기 위해 `CAP_NET_ADMIN`을 유지해야 하는 application에는 치명적이며, pid namespace와 device 생성 같은 다른 system interaction에도 문제가 생깁니다.
기존 LSM 대안
88-96Tree 안의 다른 LSM은 setid 전환을 통제하거나 `security_task_fix_setuid` hook을 사용하는 기능이 없습니다.
SELinux는 `setuid`가 현재 process에만 영향을 주고 SELinux 제어가 Linux identity attribute를 기반으로 하지 않으므로 이 operation을 제어할 필요가 없다고 설명합니다.
사용 방법
97-108이 LSM은 setid syscall을 hook하여 적용 가능한 restriction policy가 있을 때 전환이 허가되었는지 확인합니다.
Policy는 securityfs mount 위치의 `safesetid/uid_allowlist_policy`와 `safesetid/gid_allowlist_policy` file에 써서 설정합니다.
Policy 추가 형식은 literal number를 사용하고 newline으로 끝나는 `<UID>:<UID>` 또는 `<GID>:<GID>`입니다. 예를 들어 `123:456\n`입니다. 빈 string `""`을 쓰면 policy를 비웁니다.
UID/GID에 policy를 설정하면 해당 UID/GID가 user namespace UID/GID mapping 설정 같은 보조 setid privilege를 얻는 것도 막습니다.
GID policy와 setgroups()
109-118Linux v5.9에서는 기존 `CAP_SETUID` 제한과 같은 방식으로 `CAP_SETGID` privilege 제한 지원을 추가했습니다.
그러나 userspace의 일반적인 sandboxing code convention과 호환되도록 현재 `CAP_SETGID` 제한이 있는 process에도 임의의 `setgroups()` 호출을 허용합니다.
향후 release에서 `setgroups()` 제한을 지원하기 전까지 이 GID policy는 의미 있는 보안을 제공하지 않습니다. Policy checking code가 마련되면 v5.9에 추가된 GID policy configuration code를 바탕으로 `setgroups()` 제한을 강제할 예정입니다.
목적과 사용 사례
SafeSetID.rst:1-46과도한 `CAP_SETUID` 권한을 좁히는 SafeSetID의 목적을 설명합니다.