Access control
Single-page view · as markdown
Access control overview
Peios / Developing for Peios / Access control
KACS — the Kernel Access Control Subsystem — is the heart of Peios's security model, and it is the part of the SDK you are most likely to reach for. This section is a tour: it explains how the pieces fit together and points you at the right guide (and the right reference page) for each task. If you have not yet read the library conventions, read those first — everything here assumes the error and buffer rules they describe.
The four nouns #
Almost everything in KACS is built from four things:
| Noun | What it is | SDK home |
|---|---|---|
| SID | The unique binary name of a principal — a user, group, machine, or well-known system actor. | security.h |
| Security descriptor (SD) | What protects an object: its owner, its group, and the ACLs that grant or deny access. Built from ACEs, each naming a SID and an access mask. | security.h |
| Token | The runtime object that carries an identity — a user SID, groups, privileges, an integrity level, claims. Every access decision is made against a token. A token is a file descriptor. | token.h |
| Access check | The act of deciding whether a token may perform a desired access on an object, given the object's SD. | access.h |
The relationship is simple to state: an access check asks whether a token (the subject) is granted some access to an object protected by a security descriptor, and both the token and the SD are expressed in terms of SIDs.
The shape of a decision #
When you need to make an authorisation decision in your own code, the pattern is almost always the same three steps:
- Get the subject's token. Usually the caller's — often via
peios_token_open_peeron a socket, so you learn who connected — or your own effective token (token_fd = -1). - Get the object's security descriptor. You either hold it already, read it off a file with
peios_file_get_sd, or build one with apeios_sd_builder. - Run the check.
peios_access_checktells you whether the desired rights are granted, and exactly which subset was granted.
The checking access guide walks that end to end.
Advisory versus enforced #
There is one distinction worth internalising early. The SDK's access check is advisory: it computes what the answer would be. It does not enforce anything — enforcement of a real operation (opening a file, adjusting a token) happens inside the kernel against the subject's own process security block.
So you use peios_access_check when your code is the resource manager: you own some object — a record in your database, a slot in your service — that isn't a kernel object, and you want to make the grant/deny decision using Peios identities and the same rules the kernel would apply. For actual kernel objects (files, tokens, registry keys), you don't pre-check and then act; you just act, and the kernel enforces, returning EACCES if denied.
Identity is not always your identity #
A recurring theme in KACS is acting as someone other than yourself:
- Impersonation lets a service temporarily adopt a caller's identity so its access checks run as them — the way a server does work "on behalf of" a client without running as root and hand-rolling permission logic. See working with tokens.
- Restricted tokens let you drop power — derive a strictly less-privileged token to hand to less-trusted code.
- Integrity levels and confinement bound what a token can touch regardless of its SIDs.
These are what make KACS more than POSIX uid/gid, and the token module is where you reach for them.
Where to go in this section #
- Working with tokens — who am I, who is calling me, and how to act as someone else.
- Checking access — building a security descriptor and making a decision end to end.
- Securing files — the native open and reading/writing file security descriptors.
- Hardening a process — turning on process mitigations.
For the exhaustive per-function detail behind any of these, the reference section documents every symbol.
Working with tokens
Peios / Developing for Peios / Access control
A token is the runtime carrier of an identity, and it is a file descriptor. This guide covers the everyday token tasks; token.h is the exhaustive reference for every call and field.
Who am I? #
To inspect your own identity, open your effective token and query it:
int tok = ;
if
unsigned char sid;
ssize_t n = ; /* the user SID */
uint32_t il;
; /* integrity level RID */
struct peios_privilege_set privs;
; /* held/enabled privileges */
;
peios_token_open_self gives you the effective token — if your thread is impersonating, that's the impersonated identity. Pass KACS_TOKEN_OPEN_REAL in the flags to get your process's real primary token regardless. The access argument is the handle rights you want; KACS_TOKEN_QUERY is enough to read.
Group SIDs and other list-valued classes come back as buffers you parse with the security.h views: read CLASS_GROUPS with peios_token_query, then peios_sid_array_parse it.
Who is calling me? #
The most useful token trick in a service is learning the identity of whoever connected to your socket. When a client connects over a Unix stream or seqpacket socket, KACS captures their token at connect() time, and you open it from the accepted connection:
int conn = ;
int caller = ; /* QUERY | IMPERSONATE rights */
if
/* Now query the caller's identity, or impersonate them (below). */
This is local authentication with no passwords and no handshake — the kernel vouches for who is on the other end. The handle comes with fixed QUERY | IMPERSONATE rights, which is exactly what a server needs.
Acting as the caller #
Once you hold a caller's (impersonation) token, you can impersonate them: adopt their identity on your current thread so every subsequent access check runs as them, not as your service. This is how you do work on a client's behalf without running privileged and re-implementing their permissions.
if
/* ... do the work here: file opens, access checks, etc. all run as the caller ... */
; /* back to your own identity */
;
Always pair peios_token_impersonate with peios_token_revert, ideally in the cleanup path, so a failure partway through can't leave your thread wearing someone else's identity. peios_token_revert is a safe no-op if you weren't impersonating.
The full flow for a request handler is: accept → peios_token_open_peer → peios_token_impersonate → serve the request → peios_token_revert → close.
Dropping power #
To run less-trusted code with less authority than you hold, derive a restricted token and hand it over. peios_token_restrict can delete privileges, demote groups to deny-only, and add restricting SIDs:
struct peios_token_restrict spec = ;
int weak = ;
The result is a strictly less-powerful token. Combined with integrity levels and confinement (both set when minting a token), this is the basis of sandboxing on Peios.
Minting tokens #
Creating a token from scratch requires SeCreateTokenPrivilege and is the province of authentication authorities, not ordinary programs. When you do need it, the token-spec builder is the ergonomic path — typed setters for the user SID, groups, privileges, integrity, claims, and the rest, then peios_token_builder_create. Mind the index convention for owner/primary-group references, and don't add the logon SID yourself — the kernel injects it.
Next #
- Checking access — use a token to make an authorisation decision.
token.hreference — every token call in full.- Impersonation — the operator-side model and its two gates.
Checking access
Peios / Developing for Peios / Access control
This guide walks a complete access decision: you have an object to protect, a caller to check, and you want KACS to tell you what they're allowed. The exhaustive detail lives in access.h and security.h; here we put them together.
When to use this #
Reach for peios_access_check when your program is the resource manager — you own something that isn't a kernel object (a document, an API route, a record) and you want to gate it with Peios identities and rules. For actual kernel objects, don't pre-check; just perform the operation and let the kernel enforce.
Remember the check is advisory: it computes the answer, you enforce it.
Step 1 — describe what protects the object #
An object is protected by a security descriptor. If you don't already have one, build it. Say the resource should be readable and writable by its owner and read-only for a "viewers" group:
/* An ACL: allow OWNER full, allow VIEWERS read. */
peios_acl_builder *acl = ;
;
;
size_t acl_len;
const void *acl_bytes = ;
/* Wrap it in a security descriptor with an owner. */
peios_sd_builder *sd = ;
;
;
size_t sd_len;
const void *sd_bytes = ;
(You can also write the descriptor as SDDL text and parse it — often easier when the policy is fixed.)
Step 2 — identify the subject #
The subject is a token. Most often it's the caller you opened from a socket, or your own effective token. In the request you pass either a token fd or -1 for your own effective token.
Step 3 — run the check #
Fill in a request and call. desired is what you're testing; mapping folds any generic rights to the object class's specific bits (use the class's published mapping — here we'll treat the object like a file):
struct peios_access_request req = ;
uint32_t granted = 0;
int rc = ;
Step 4 — interpret the result #
if else if else
The key idea: a denial is not an error to log and bail on — it's the expected "no". And because granted is filled even on denial, you can ask for a broad set of rights in one call and read back exactly which subset the subject has, rather than probing right by right. Clean up the builders (peios_acl_builder_free, peios_sd_builder_free) and the token fd when done.
Per-property checks #
If your object has properties or property-sets with their own object ACEs, evaluate the whole tree in one call with peios_access_check_list: supply an object-type tree and get one result per node, so you learn (for instance) that a caller may read most of an object but not one protected field — without a separate check per field.
Auditing a decision #
Pass a non-NULL peios_access_audit to learn what a SYSTEM_AUDIT ACE match would log, and whether a staged central access policy would decide differently — the signal you watch when rolling out a policy change.
Next #
access.hreference — every field of the request and the audit outputs.- Access decisions — the operator-side account of how KACS reaches a verdict (and how to debug a surprising denial).
Securing files
Peios / Developing for Peios / Access control
Peios files carry real security descriptors, and the SDK opens them with a native KACS open rather than POSIX open(). This guide covers the two everyday tasks: opening a file with a specific access, and reading or changing a file's security. The full surface is in file.h.
To keep the fragments readable, most error checks are elided here — every call below returns -1 with errno on failure, and real code must check each one (see Library conventions).
The native open #
peios_file_open is shaped like NtCreateFile: you state the access you want, what to do about existence (the disposition), any create options, and — when creating — the security descriptor to stamp on the new file. It returns an ordinary Linux fd whose granted access is fixed for the fd's lifetime, which means you can safely hand it to another process by SCM_RIGHTS, dup, or across exec: the fd carries exactly the access it was opened with.
Open-or-create a file, readable and writable, stamping a creator SD if it's new:
struct peios_open_params p = ;
uint32_t status = 0;
int fd = ;
if
if
else
status_out tells you what happened — created versus opened versus overwritten — without a separate stat and its attendant race. If you're only ever opening existing files, use a plain open disposition and pass sd = NULL.
Reading a file's security descriptor #
To see who can do what to a file, read its SD. secinfo selects which components you want — owner, group, DACL, SACL — so you fetch only what you need:
/* Probe, allocate, read (two-call). */
ssize_t need = ;
void *sd = ;
;
/* Parse it with a security.h view. */
peios_sd_view v;
;
peios_acl_view dacl;
if
;
If you already hold a file fd, use the fd-targeted peios_fd_get_sd instead of a path — no second path resolution, and for a normal file fd the check uses the access already baked in at open.
Changing a file's security descriptor #
Writing an SD is component-selective too: name the components you're changing in secinfo, and everything you don't name is preserved. To tighten a file's DACL without touching its owner or SACL:
/* Build an SD carrying only a DACL. */
peios_sd_builder *b = ;
;
size_t sd_len; const void *sd_bytes = ;
;
;
Because only KACS_SECINFO_DACL is selected, the owner, group, and SACL are left exactly as they were. The security.h builders are how you assemble the SD to apply.
Pre-flighting an open #
Sometimes you want to know whether a caller could open a file before you actually do. Read the file's SD, then run an access check against the caller's token with peios_file_generic_mapping — no open, no side effects, just the verdict.
Next #
file.hreference — every parameter, plus the fd-targeted calls and mount policy.- File access — the operator-side model of native file security.
Hardening a process
Peios / Developing for Peios / Access control
Peios lets a process opt into hardening — exploit mitigations enforced by the kernel on that process's security block. The SDK exposes this through a single call, peios_process_set_mitigations. This short guide covers using it well; the full flag set and semantics are in process.h and the operator docs.
Harden yourself at startup #
The common case is a program hardening itself early in main, before it processes any untrusted input:
int
pidfd == -1 targets the calling process. mitigations is a mask of KACS_MIT_* bits (from <pkm/psb.h>); combine the ones you want and set them together.
Three things to know #
It's one-way. Mitigation bits can only be set, never cleared — once on, they stay on for the life of the process. That's the point: a mitigation you could turn off is one an attacker could turn off. Treat each call as a permanent commitment.
It's all-or-nothing, and fails closed. If any requested protection can't actually be activated, the call changes nothing and returns -1. You never end up believing a mitigation is on when it isn't. So request the set you require together and check the result once — success means the whole set is active. (Bits from earlier successful calls stay on regardless.)
Targeting another process is privileged. To harden a process other than your own, you need PROCESS_SET_INFORMATION on it and PIP dominance over it — you can't reach into a process you don't already dominate. For self-hardening (-1), neither is needed.
Choosing what to enable #
The bit catalogue and what each mitigation defends against is documented operator-side under process mitigations (and in the Peios Kernel TRM §3.3, the Process Security Block). A couple of notes for the SDK caller:
KACS_MIT_ALLis the mask of all valid bits — useful for validating input, not usually what you'd blanket-enable without thought.KACS_MIT_CFIis a legacy alias that expands toKACS_MIT_CFIF | KACS_MIT_CFIB.
Enable the specific protections your program can tolerate, verify the call succeeded, and prefer to do it before you touch untrusted data.
Next #
process.hreference — the call in full.- Process mitigations — every mitigation and its threat model.