Security model¶
As a client, sdvarlink adds no privilege and makes no authorization
decisions: it connects to sockets systemd created, sends what you ask, and
decodes what comes back, defensively. When you serve with varlink.Server,
authorization is yours: see Serving a service below.
Authorization stays with systemd¶
flowchart LR
A["Your process<br/>uid=1000"] -- "connect(2)" --> S["/run/systemd/io.systemd.Hostname<br/>(socket file permissions)"]
S --> D["systemd-hostnamed"]
D -- "SO_PEERCRED: who is calling?" --> D
D -- "privileged method?" --> P{"polkit"}
P -- "allowed" --> OK["reply"]
P -- "denied" --> E["error reply:<br/>PermissionDenied / io.systemd.System EPERM"]
- Socket permissions. The kernel checks the socket file's mode and owner
before
connect(2)succeeds. Some sockets are root-only (for example journald's/run/systemd/journal/io.systemd.journalis mode0600). - Peer credentials. The service reads the caller's UID, GID and PID from
the socket (
SO_PEERCRED). sdvarlink can't forge them, and doesn't try. - polkit. For privileged operations, the service asks polkit. Methods with
an
allowInteractiveAuthenticationparameter can trigger an interactive prompt if you set it totrue; it defaults tofalse.
So a program built with sdvarlink can do exactly what varlinkctl run as the
same user can do. Denials arrive as errors you can match:
err := c.Rotate(ctx)
if errors.Is(err, varlink.ErrPermissionDenied) || errors.Is(err, syscall.EPERM) || errors.Is(err, syscall.EACCES) {
return fmt.Errorf("rotate needs root or a polkit grant: %w", err)
}
Limits on what a peer can send¶
A misbehaving or compromised peer can't make your process allocate without bound:
| Limit | Default | Option | On violation |
|---|---|---|---|
| Message (frame) size | 16 MiB (varlink.DefaultMaxFrameSize) |
varlink.WithMaxFrameSize(n) |
varlink.ErrFrameTooLarge; connection closed |
| File descriptors per read | 16 (varlink.DefaultMaxFiles) |
varlink.WithMaxFiles(n) |
varlink.ErrTooManyFiles; received fds closed; connection closed |
| Malformed JSON / protocol violations | n/a | n/a | *varlink.ProtocolError; connection closed |
// Tighter limits for a service you only expect small replies from.
c, err := hostname.Dial(ctx, varlink.WithMaxFrameSize(1<<20), varlink.WithMaxFiles(0))
if err != nil {
return err
}
defer c.Close()
Use contexts with deadlines so a stalled peer can't block you indefinitely; see Concurrency: contexts and deadlines.
Decoding¶
- Replies are decoded into generated structs with
encoding/json. Fields the bindings don't know are kept in each struct'sExtramap, never silently dropped and never interpreted. - Optional fields are pointers, so "absent" is distinguishable from "zero".
- Type mismatches are decode errors, not silent coercions.
- Error parameters for unknown error names stay raw (
*varlink.Error.Parameters) until you decode them.
File descriptors¶
Descriptors are capabilities, so sdvarlink is strict about them:
- Received descriptors get
FD_CLOEXECimmediately, so they can't leak into child processes youexec. - Unclaimed descriptors are closed. A reply's descriptors are given only to the fd fields that reference them; any others, including those attached to error replies, or to replies you discard, are closed.
- Every error path closes what it received: truncated ancillary data
(
MSG_CTRUNC), bad indexes, decode failures. Tests count open descriptors across thousands of calls to enforce this. - Descriptors you send stay yours. sdvarlink never closes, duplicates or
retains them past the
sendmsg(2).
See File descriptors for the full ownership rules.
Logging¶
sdvarlink logs nothing. Parameters can hold secrets, such as passwords for
io.systemd.MountFileSystem.MountImage, credentials for io.systemd.Credentials
or user records, so the library never writes parameters, replies or errors
anywhere. If you log errors, remember that a *varlink.Error's message
includes its parameters.
Transport scope¶
sdvarlink speaks Varlink over local Unix sockets only. There is no TCP, TLS
or remote transport, and no org.varlink.resolver: systemd uses fixed socket
paths. For remote access, put a purpose-built bridge (such as
varlink-http-bridge) and its own authentication in front.
Dependencies and supply chain¶
The runtime depends on the Go standard library and golang.org/x/sys only,
with no cgo. Generated code is checked in and reviewed like any other code,
and CI fails if it drifts from what the generator produces
(make check-generated).
Reporting a vulnerability¶
Please report security issues privately through GitHub security advisories rather than in a public issue.
Serving a service¶
When your program serves Varlink with varlink.Server, the roles flip:
clients are untrusted input and your handlers decide who may do what.
- Authorize in the handler.
call.Peer()gives the caller's UID, GID and PID fromSO_PEERCRED. Decide on UID and GID; PIDs are reused. Returnvarlink.ErrPermissionDeniedto refuse. - Restrict the socket. Set
SocketMode=andSocketGroup=in the.socketunit, orvarlink.WithSocketMode/varlink.WithSocketOwnerwithvarlink.Listen(default mode0660), so only the right users can connect at all. - Parameters are validated before handlers run, against the IDL: missing,
mistyped and unknown fields are refused with
InvalidParameter. - Internal errors don't leak. A handler error that is not a Varlink error
reaches the client as
io.systemd.SystemEIO; the detail goes to the log. Panics are recovered the same way. - Resource limits. Calls over 16 MiB close the connection, at most 16
descriptors are accepted per read, and at most 256 connections are served
at once (
WithServerMaxFrameSize,WithServerMaxFiles,WithMaxConnections).WithIdleTimeoutcloses idle connections. - No parameter logging. The server logs method names and errors, never call parameters.