Files
schedls/docs/security-model.md
T
mdaleo404 aded37d491
CI / test (3.11) (push) Canceled after 0s
CI / test (3.12) (push) Canceled after 0s
CI / test (3.13) (push) Canceled after 0s
CI / test (3.14) (push) Canceled after 0s
CI / package (push) Canceled after 0s
CI / precommit-and-security (push) Canceled after 11s
CI / typecheck (push) Canceled after 11s
Initial code commit
2026-09-25 11:16:19 +01:00

111 lines
4.8 KiB
Markdown

# Security model
This document describes what `schedls` trusts, what it does not, and how the
main threats are mitigated. It complements [SECURITY.md](../SECURITY.md).
## Trust model
- **Native schedulers are authoritative.** systemd and cron own execution.
`schedls` only reads and writes their configuration.
- **Discovered configuration is data.** Commands and schedules found on the
host are parsed and displayed, never executed.
- **Unmanaged objects are protected.** A timer or cron entry not created by
`schedls` cannot be modified or removed by default.
- **The user chooses privilege.** `schedls` never escalates on the user's
behalf.
## Threats and mitigations
### Malicious schedule names
Names must match `[A-Za-z0-9][A-Za-z0-9_.-]{0,63}`. Path separators, traversal,
whitespace and shell metacharacters are rejected. Names are never interpolated
into shell strings.
### Malicious command arguments
Arguments are kept as an argv internally. systemd units are rendered with a
dedicated serializer that always quotes arguments and escapes backslash,
double quotes, literal `$` and `%`; arguments containing NUL or newline are
rejected. cron commands are serialized with shell quoting and then have every
`%` escaped for cron's own parser. Calendar expressions are validated with
`systemd-analyze` and must not contain control characters or begin with `-`;
timer durations (`--jitter`, `--accuracy`) are validated with
`systemd-analyze timespan`. If a safe representation cannot be guaranteed,
creation is refused.
### Symlink attacks during privileged writes
Before replacing a file, `schedls` uses `lstat`, rejects symlinks and
non-regular files, and verifies ownership. Destination directories must be
real directories owned by the expected user (`root` when running as root) and
must not be group- or other-writable. Temporary files are created in the same
directory and atomically renamed.
### PATH hijacking
Critical helpers (`systemctl`, `systemd-analyze`, `journalctl`, `crontab`,
`loginctl`) are resolved once with a controlled lookup. A helper is rejected
unless the executable **and** its realpath target are regular files, are owned
by root (when `schedls` runs as root) or by root or the current user, and live
in a directory that is not group- or other-writable. Helper processes run with
a fixed `PATH` rather than the caller's.
### Path traversal through discovered unit names
A `Unit=` value read from a timer file is treated as untrusted data. Mutation
never uses it to build a filesystem path: unit names are always derived from
the validated job name (`schedls-<name>.timer` / `.service`). Generated unit
names are re-validated before they are written to a temporary directory, so a
crafted unit cannot redirect writes or removals outside the trusted unit
directory.
### Untrusted output
`journalctl` output is program output, not configuration. When stdout is a
terminal, control characters are escaped before display so a log line cannot
inject terminal escape sequences; redirected output is passed through
unchanged. Machine-readable `--json` output escapes control characters as part
of JSON encoding.
### Environment injection and pagers
Helper commands run with a controlled environment (`LC_ALL=C`,
`SYSTEMD_COLORS=0`, `SYSTEMD_PAGER=cat`, `PAGER=cat`) and never with a shell.
Pagers and editors are never launched while `schedls` is running. Environment
values passed with `--env` are written to unit files with systemd quoting and
are world-readable like any other unit; do not put secrets in them.
### TOCTOU races
Plans snapshot existing content. At apply time the current content is compared
against the snapshot; a mismatch aborts the operation. Privileged critical
sections are kept small and use file descriptors / atomic rename where
practical.
### Corrupt or malicious existing configuration
Configuration is treated as opaque data. Malformed `schedls` cron markers cause
mutation to fail closed rather than guess. Unknown crontab lines are preserved
byte-for-byte. A systemd unit whose name starts with `schedls-` is only treated
as managed when its file carries the `# Managed by schedls` and `# Name:`
markers, so a same-named foreign unit is not modified.
### Partial operations
systemd creation writes both units, reloads the manager, and enables the timer;
if a later step fails, or the operation is interrupted, the previous state is
restored and a partially enabled timer is disabled again. Cron installs are
validated first and restored from the previous crontab text on failure.
### Supply chain
Runtime dependencies are zero. Development dependencies are locked and audited
in CI.
## Availability, privacy
`0.1.0` requires `systemd-analyze` to create or validate calendar expressions
and refuses the operation if it is unavailable. `schedls` makes no network
requests and collects no telemetry.