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
111 lines
4.8 KiB
Markdown
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.
|