Reference
Command line#
ljos-policyd check|exec|capnp|version -- argv...
ljos-policyd check-mail --config PATH [--argv [--] RECIPIENT...]
reads one RFC 5322 message on stdin. See “Outbound mail” below.
Verb |
Meaning |
|---|---|
|
print |
|
write a packed Cap’n |
|
same text verdict; on allow, run argv; on deny, print the verdict on stderr and exit 2 |
|
JSON |
|
print |
A leading -- after the verb is optional and is stripped.
Built-in denials#
First match wins. The head is the last path component of argv[0].
Leading assignments, NAME=value or NAME+=value, and runner words
such as env, nice and timeout come off first, as does the head
of a case arm (case $x in a), or b) after ;;). A trap
action is judged as a script of its own. A shell with no -c script
and no file runs what it reads on stdin: the text of a here-string or
heredoc is judged as that script, and a pipe, a -s shell fed
neither, or a here-string holding an expansion is refused as
shell-stdin-unshown. A command word that an alias earlier on the
line names is read as the alias’s body, since dash and zsh expand
aliases in a script and the line does not say which shell reads it.
Token |
Cap’n PolicyReason |
When |
|---|---|---|
|
|
no program |
|
|
privilege runner |
|
|
a download run as a program |
|
|
recursive delete off tmp |
|
|
force push or guarded lease |
|
|
setuid or setgid mode |
|
|
dd to a device node, or mkfs |
Anything else is allow. python -c is allow. Schema: schema/policy.capnp.
Task runners#
A line that runs a task runner is also judged on what the target runs.
The recipe comes from the file the runner reads, in the directory it
runs in: the working directory, the one a cd or pushd earlier on
the line enters, the one env -C names for the command it runs, or
the one the runner’s own -C, --prefix or --cwd names.
env reads its flags as GNU env does: short ones cluster
(-iC DIR, -uVAR -C DIR) and a long one may be cut to a prefix that
names only it (--ch DIR), so these move the command as -C DIR
does. -S STRING (or -iS STRING) splits STRING into words that take
its place, so options in it (env -S '-C dir' make) and the words
after it are read too. The shell expands STRING first, so a $NAME or
${NAME} in it that the line assigned one value earlier, and sets no
other way, is read as that value. A string that still holds a $ after
that may run any command, so it is refused as
env-split-string-unshown.
Leading words that still run cd in this shell (command,
builtin, if, while, { and the other control keywords, a
case arm’s head, and
the time keyword with its -p, which is a program after command
or builtin, where no word is a keyword)
are peeled before this check, as are assignments among them
(if FOO=1 cd dir), and a redirection among the words of a
cd or pushd names neither the directory nor a pushd stack index.
One that closes a descriptor (2>&-, <&-) is a whole word, so the
word after it is the target.
Runner |
File read |
|---|---|
|
|
|
|
|
|
|
the |
Each recipe line is judged as a sh -c script, and a refusal names the
target and the line: make clean-all runs `rm -rf ~`: rm-rf-outside-tmp.
Nothing is spawned. A missing or unreadable file leaves the line
judged as written. A cd or pushd to a path the line does not show
(cd "$X") leaves it in a directory it does not follow, so a runner
after it is refused. So does an eval, a source or . whose file
the guard cannot read or whose text may move the shell or set one of
those, a command word the line does not show ($c dir), which may be
cd, as may an alias whose body the line does not show, and a trap
whose action is one of those or a cd, pushd or popd, since a
DEBUG trap runs before each command; after one HOME, CDPATH and
PWD are not followed either. A source or . of a literal path to
a regular file the guard can read, whose text does none of that, leaves
the line where it was. The text is read with the aliases the line
defined before the source expanded, as the shell that sources it
reads it.
Raw newlines and carriage returns split script commands, and $'...'
ANSI-C quoted words are decoded before the line is judged. A backslash
before a newline joins the two lines, outside single quotes, as the
shell joins them.
A cd or pushd to a relative path whose first part is not . or
.. enters that path under the first CDPATH entry that holds it, an
empty or . entry being the working directory, so the runner after it
is read there, and in any entry before it the line does not show. A
CDPATH set in
front of the cd (CDPATH=dir cd x), or of the shell that runs it, is
the one searched, as is one the line sets before it, alone or with
export, declare and the like (export CDPATH=dir; cd x), unless a
pipeline stage, a subshell or a list sent to the background set it.
One set where it may not run, past a && or ||, is searched along
with the one before it. Another command that writes it in this shell
(read CDPATH, printf -v CDPATH, ${CDPATH:=dir}, or an
assignment in front of a special builtin such as :), or an entry with
$, a backquote or ~ in it, names no directory the line shows, so
a runner after the cd is refused. A word that only names it
(grep CDPATH, env CDPATH=dir cmd) changes nothing.
HOME is followed the same way: a bare cd enters the one set in
front of it (HOME=dir cd) or earlier on the line (export HOME=dir; cd), and ~ expands from the one set before the command, as do
$HOME and ${HOME} at the head of a path. A HOME
that may hold either of two values, or one the line does not show
(HOME=$(mktemp -d) cd), leaves the runner after the cd refused.
$PWD in either value is the one directory the line is in, when it
shows that and does not set PWD (export HOME”$PWD/.home”). With =HOME unset, ~ is the home in the user’s passwd entry, as in
bash, and a bare cd stays. With it unset or empty, $HOME/path is
the path from /, as is ~/path with it empty, while
cd $HOME, or cd ~ with it empty, stays.
~login expands from the password database and ~+ to where the
line is, while ~- and a stack entry such as ~1 name a directory
that is not followed.
A cd or pushd takes .. off the path by name, as the shell’s
logical cd does, so cd lnk/.. comes back to where it started even
when lnk is a symbolic link. Where that names no directory, the
line may instead be through the link, as bash then tries the path as
written (cd lnk/../x with x only beside the link’s target).
cd -P, the last of -L and -P winning, resolves the link
first, and after a set -P or set -o physical on the line, or a
SHELLOPTS that may name physical for a bash the line starts, either
may hold. A source or . whose file runs set -P leaves the line in
a directory it does not follow. env -C resolves it, as chdir does.
pushd and popd follow the directory stack. pushd -n stacks its
directory without entering it, pushd +N and -N enter the entry they
bring to the top, and a bare popd enters the entry under the top,
while popd -n, popd +N and popd -N drop an entry and stay. An
index that counts past a pushd that may have failed may name any
directory on the stack. A popd that fails, on a word it does not take
(popd x) or with nothing left to pop (after dirs -c), leaves the
line in a directory it does not follow. Under shopt -s lastpipe or
set -o lastpipe, alone or in an eval, or a shopt with a word
the line does not show (shopt -s $opt), and in a shell the line
starts after setting BASHOPTS or SHELLOPTS to a value that names
lastpipe or that it does not show, a
cd, pushd or popd in the last stage of a pipeline does the same,
and a dirs there leaves the stack not followed.
The line goes on past a cd that may fail, so the runner after it is
read in each directory the line may be in, up to sixteen. A cd into a
directory without the search bit moves nothing. A runner that may run
in a directory past those sixteen is refused, as
make clean-all may run in an untracked directory.
A cd - or pushd - enters OLDPWD, which each cd, pushd and
popd on the line moves, and a script the line runs starts with the
one its command has. Without OLDPWD in the environment, and in a
recipe’s line, where it is not followed, a runner after cd - is
refused the same way.
The words xargs adds from stdin can name any target, so a runner that
xargs runs is refused, as xargs make may take a target from stdin.
Argument limits#
A phronesis build hands each word to the pack whole. phronesis 0.2.3
keeps a word of up to 128 KiB (131072 bytes) and up to 32768 words. A
Cap’n command body over 512 KiB (524288 bytes) is refused before the
word count is checked. Past a limit the answer is field-too-long and
the host’s reason:
Past |
Answer |
|---|---|
a word longer than 131072 bytes |
|
more than 32768 words |
|
a command body longer than 524288 bytes |
|
A blank reason is the name alone, field-too-long. The limits are
PHRONESIS_SHELL_WORD_MAX and PHRONESIS_SHELL_ARGV_MAX in phronesis
src/internal.h, and PHRONESIS_CAPNP_MAX_BODY in phronesis
include/phronesis/supervisor.h. phronesis 0.2.3 sets them to 131072,
32768 and 524288 and writes the reason strings above. This binary names
the code and keeps the reason. Release binaries link phronesis 0.2.5.
A build without the library uses the host table and does not apply
these limits.
Outbound mail#
check-mail prints one JSON object and only refuses. allow means
this check found nothing to refuse. A send still needs its own grant.
The message on stdin is the whole input. A caller must pass no
recipient arguments to sendmail -t: those addresses are not in
the message. Pass the same recipients to --argv and this check
refuses them as recipients. --config comes before --argv.
The config is lines of identity, deny-file and pack-law,
an optional subaddress line, and any number of alias lines. A
relative path is read from the config file’s directory. identity is
the one address From must be, after ASCII case folding only. A
local part or domain that NFKC or the removal of a skipped
character would change is a refusal, including a zero-width
character or a fullwidth spelling. The mailbox is the address in
angle brackets, read from the raw unfolded value. A display-name encoded-word is decoded for display only, and
an encoded addr-spec is a refusal. A second From is a refusal.
A group in From is a refusal. A group in a recipient header
still contributes its mailboxes.
Reply-To, Sender, Return-Path, Errors-To,
Disposition-Notification-To, Return-Receipt-To,
Delivery-Receipt-To, Mail-Followup-To and Mail-Reply-To
must stay clear of a deny-file identity (address, or a mailbox
at a domain) and of a domain named by the pack-law file.
Sender, Errors-To and Return-Path, when present, must
be the identity under the same rule as From. An empty
Return-Path is not that address. Any Resent-* header is a refusal, and so is
any Apparently-To. To, Cc and Bcc are refused when
the mailbox is an address line in the deny file, and when the
domain or a parent of it is named by the deny file or by a
deny-domain line in the pack-law file. An address line is
that mailbox, not the whole domain. It also covers that local
part, one subaddress separator, and anything after the
separator. The separators are the subaddress line, or +-
when the line is omitted. ., @, % and ! are not
separators. An address rule is also compared by skeleton:
NFD, combining marks removed, case folded twice, combining marks
removed again, then NFC, with every ., _, -, + and =
removed and nothing cut. The second fold turns capital sharp s
into ss. The second removal drops the dot that case folding İ
produces, so İ and i are one skeleton, and a mark cannot compose
with the letter before it and escape the prefix. abc followed
by U+0301 and x is denied by abc, and Lithuanian i + U+0307 +
U+0300 is the same skeleton as ì. A recipient whose skeleton starts with
the rule’s skeleton, at the rule’s domain or one in the same
alias family, is refused, and so is one the folding below
refuses. This over-blocks on purpose: a rule for john also
refuses johnny and jo.hn-x, so a provider that ignores a
character no family lists cannot deliver a denied mailbox.
An address rule whose skeleton is empty refuses the deny file.
The error names the line and not the address. A deny file that
does not load refuses every send. A skeleton shorter than three
characters still matches that mailbox, its subaddresses and its
alias family, and does not use the prefix, so jo denies jo
and jo+tag and not john.
A provider alias family folds both the rule and the
recipient, and adds its separators to the subaddress line on
its domains. gmail.com and googlemail.com are one
domain, dots in the local part are ignored, and + is a
separator. On fastmail.com, + is a separator and a
mailbox at <local>@<user>.fastmail.com is
<user>+<local>@fastmail.com. Every label in front of
fastmail.com is the user, and a hyphen there is part of the
user name. A punycode user label is decoded to Unicode and folded as a
local part, so x@xn--bcher-kva.fastmail.com is
bücher+x@fastmail.com. proton.me,
protonmail.com, protonmail.ch and pm.me are one
domain, and so are icloud.com, me.com and mac.com,
each with +. outlook.*, hotmail.*, live.* and
msn.* (every domain with that first label, whatever the
country suffix) are separate domains, each with +. Exact and
subaddress matching stay on the domain the rule names. The
skeleton prefix still crosses that family, so a rule for
john@outlook.com also denies john@hotmail.com,
john@live.com, john@msn.com and john@outlook.de. That
over-block is the prefix, not a shared mailbox. On
yahoo.*, ymail.* and rocketmail.com, - is a
separator, and an address rule
is also cut at its first separator, so a rule for one disposable
address <base>-<keyword> covers <base> and every keyword.
An alias line adds a family to these:
alias <domain>[,<domain>...] <separators> [flag...]. A
domain may be <label>.*, every domain whose first label is
<label>. The separators are checked as on the subaddress line, and the flags
are shared (the domains, and the same subdomain of each, are
one mailbox), dots (dots in the local part are ignored),
subdomain (<local>@<user>.<domain> is
<user><sep><local>@<domain>, <sep> being the line’s first
separator, folded again so dots also reaches the user) and cut (the rule is also cut at its
first separator). An alias line cannot remove a built-in
family, and one that does not parse, or names one domain or flag
twice, refuses the config. A
domain rule on a domain of a shared family covers every
domain of that family and their subdomains, so domain gmail.com
also refuses googlemail.com, domain pm.me the other Proton
domains, and domain icloud.com me.com and mac.com.
deny-domain in the pack-law file does the same. A domain
or deny-domain value has to be a hostname after IDNA. A star,
a colon, a slash, an at-sign, whitespace, or anything else that
is not a hostname refuses the file. So does a domain a
recipient cannot have, such as an all-numeric one, in a
domain, deny-domain or address value. The detail is the
line number and not the value. A wildcard is not how to name
subdomains: domain example.test already covers
a.example.test. An address value with a star, or with a
character a parsed recipient cannot carry, is refused the same
way. From is
not matched this way. A domain literal (an address
at @[ ... ]) in a watched header is a refusal. So is a domain
that is not a hostname after IDNA, such as one with an underscore,
a ! or a +, a label that starts or ends with a hyphen, a label
over 63 characters or a name over 253: no rule could name it. An
all-numeric domain is a refusal, judged after IDNA. That
includes a dotted quad, a shorter form inetaton accepts, a
hex label (0x) and a label with a leading zero. U+3002 or
U+FF61 between the numbers is still that address. A source route
(<@host:mailbox>) is a refusal. @, % or ! in a local
part is a refusal, quoted or not, including a backslash escape
inside the quotes. A local part that NFKC changes into ASCII
punctuation, such as a fullwidth @, %, ! or +, is a
refusal. A local part that contains a default ignorable code point,
a noncharacter, an unassigned or private-use code point, a format
or control character, or a space separator is a refusal. A
zero-width space or a braille blank is still removed before the
address is compared.
A recipient domain, or a domain on a header checked against the
deny file, is refused when one label mixes Latin with Cyrillic or
Greek after NFKC and IDNA.
A deny-file domain compares a fullwidth spelling and the punycode
form as the same domain. A homoglyph that NFKC and IDNA leave alone
does not, and a label wholly in Cyrillic or Greek is not mapped to
the Latin it looks like. Case is folded as the address skeleton
folds it, so ß is ss and ı and İ are i, and a character a reader
skips is removed before that comparison. From is not compared that way. handle, term and pair may share the deny file and
are ignored here. A pack-law line that would allow a domain refuses
the file. The detail of deny-file-invalid and pack-law-invalid
is the line number, not the value. A message over 1 MiB, a control character in the header
block, U+0085, U+2028 or U+2029 in that block, a header line over
998 octets, a header block that mixes CRLF and a bare LF, or a
config that cannot be read is a refusal.
Code |
When |
|---|---|
|
no From |
|
more than one From |
|
From is not the configured identity |
|
From does not parse as one mailbox |
|
From uses group syntax |
|
an encoded-word in a watched header did not decode |
|
Reply-To names a deny-file identity |
|
Reply-To does not parse |
|
Sender names a deny-file identity |
|
Sender does not parse |
|
Sender is present and is not the identity |
|
Return-Path names a deny-file identity |
|
Return-Path does not parse |
|
Return-Path is present and is not the identity |
|
Errors-To names a deny-file identity |
|
Errors-To does not parse |
|
Errors-To is present and is not the identity |
|
Disposition-Notification-To names a deny-file identity |
|
Disposition-Notification-To does not parse |
|
Return-Receipt-To names a deny-file identity |
|
Return-Receipt-To does not parse |
|
Delivery-Receipt-To names a deny-file identity |
|
Delivery-Receipt-To does not parse |
|
Mail-Followup-To names a deny-file identity |
|
Mail-Followup-To does not parse |
|
Mail-Reply-To names a deny-file identity |
|
Mail-Reply-To does not parse |
|
an Apparently-To header is present |
|
a Resent-* header is present |
|
a label mixes Latin with Cyrillic or Greek |
|
To, Cc or Bcc names a deny-file address |
|
To, Cc or Bcc is at a denied domain |
|
To, Cc or Bcc does not parse |
|
an argv recipient is a denied address or domain |
|
an argv recipient does not parse |
|
a control, or U+0085, U+2028 or U+2029 |
|
the header block mixes CRLF and a bare LF |
|
a header line is over 998 octets |
|
the message is over 1 MiB |
|
the bytes are not a header block |
|
the config path cannot be read |
|
the config is missing a key or names an unknown one |
|
the configured identity is not one mailbox |
|
the deny file cannot be read |
|
a deny-file line is not a known kind, a domain is not a hostname, or an address skeleton is empty. The detail is the line number |
|
the pack-law file cannot be read |
|
a pack-law line is not deny-domain, or the domain is not a hostname. The detail is the line number |
Environment the seat reads#
These are not read by this binary. They are how ljos finds it.
Variable |
Meaning |
|---|---|
|
absolute path to this binary |
|
if set to |
Exit statuses#
Code |
Meaning |
|---|---|
0 |
allow, check-mail with nothing to refuse, or |
2 |
deny, a check-mail refusal, missing verb, or empty usage |
127 |
|
other |
|
What this crate is#
Remember and Prefer stay in packset.
A check is the argv verdict. Pack rules stay in packset.
ljos policyprints the line and composes this verdict with pack rules.