Command reference
Every command in both binaries, grouped as they group themselves, and which of them still opens the database.
cardinal -h and cardinal-server -h print these with the reasoning attached.
Reproduced here so they are searchable, and because knowing a command exists is
most of the problem.
Two binaries
cardinal-server runs Cardinal and does what has to happen against the
database before anybody can sign in. It is what the container image carries.
cardinal is the administrative CLI. It is not in the image, so a shell
in a running container does not hand somebody a working admin tool. See
Two ways in for what that is worth, which is less than
it sounds.
Getting them
Each release page has an archive per platform: cardinal_<version>_<os>_<arch>.tar.gz
for Linux and macOS, on amd64 and arm64, holding both binaries, and a
checksums.txt. Check the archive before unpacking it:
sha256sum -c checksums.txt --ignore-missing # Linux
shasum -a 256 -c checksums.txt --ignore-missing # macOS
On macOS, the binaries are not notarised, so Gatekeeper blocks a copy a
browser downloaded, as being from an unidentified developer. They are signed
only ad hoc, which lets Apple silicon run them but names no developer. A
browser marks what it downloads with the quarantine attribute, and unpacking
the archive with tar carries the mark onto both binaries. Once the checksum
matches, remove it:
xattr -d com.apple.quarantine cardinal cardinal-server
A copy downloaded with curl, or another command-line tool, carries no mark
and runs as it is. That was measured on macOS 26.6 with 0.6.2. Notarising the
binaries, so that neither step is needed, is
deferred until
Cardinal has a public release.
These reach the API, signing in with a passkey:
- Membership:
grant,revoke,members,memberships,history - Entities:
<type> create,<type> disable,<type> enable, for every type including hosts - Applications: every
appsubcommand - POSIX identity: every
posixsubcommand, includingposix adopt - Access tokens:
token create,listandrevoke - Reading the directory:
listandshow - Notification email: every
mailsubcommand ssh,host joinandhost whoami- This terminal's session:
statusandlogout
What still opens the database is what has to work when nobody can sign in
(invite, policy activate, decisions, redact) and ssf, oidc and the
certificate authorities, which have not moved yet.
Granting that way needs a passkey used within the administrative window, on the device that is bound to the account. That is deliberate and it has a consequence worth knowing before you script anything: no unattended process can grant a membership. A pipeline that needs to is asking for the database credential, which is the thing that owns the directory outright.
Staying signed in
The first command that needs the server signs in through your browser: a page
asks you to approve the terminal, or, on a machine you reached over SSH, the
terminal prints a code to approve from any browser. From Cardinal 0.7.0 the
session lasts like the console's, and it is kept in the macOS Keychain or the
Secret Service. Where neither exists, as on a server reached over SSH, it is
kept in a file under XDG_RUNTIME_DIR, which ends with your login session;
where that does not exist either, nothing is kept and every command signs in.
CARDINAL_SESSION_STORE=keyring, file or none chooses.
Signing out of the browser that approved a terminal signs the terminal out too, and the console's session list shows terminals as such.
An administrative command still needs a security key used within the
administrative window. When one is refused for that reason, cardinal asks
you to approve a step-up in the browser, which requires your key there, and
runs the command once more.
status Who this terminal is signed in as, until when, and whether
administration is open. Never starts a sign-in
logout End this terminal's session, on the server and here
The first command after upgrading from 0.6 ends the sessions kept in the old
~/.config/cardinal/sessions.json, removes the file, and says where sessions
are kept from then on.
cardinal-server
serve [-config <file>] [-dev] Run the API and admin UI
migrate [-status] Apply the embedded schema
init <login> [-display <text>] First-run: policy, the first administrator,
and an enrolment link
-policy <file> publish this instead of the built-in set
-no-policy publish nothing, leaving default-deny
config [-config <file>] [-all] The effective configuration, and where each
value came from
sessions end-all End every session, everyone's: all sign out,
console and command line. Access tokens stay
healthcheck [-config <file>] Exit 0 only if the server beside it answers
/api/health with status ok (the image's
HEALTHCHECK)
Entities
user create <name> [-display <text>]
group create <name> [-display <text>] [-app <application>]
host create <name> [-display <text>]
<type> disable <name> Cut an account off. Sessions and access
tokens are revoked with it.
<type> enable <name>
list [type] [-all] -all includes disabled
show <type> <name> One entity and its memberships
Enrolment and membership
invite <login> [-for <duration>] Print a single-use enrolment link
invite list
invite revoke <login>
grant <group> <member> [flags]
-for <duration> e.g. 30d or 72h — bounded, and preferred
-until <RFC3339>
-reason <text> preserved even after revocation
revoke <group> <member> [-at <RFC3339>]
members <group> [-at <RFC3339>]
memberships <user> [-at <RFC3339>]
history <group> <member> [-at <RFC3339>]
Applications
app register <name> [-redirect <uri>] With redirect URIs it is an OIDC relying
party; without, an entity to write policy
about and a name a hostname belongs to
app list Every application, relying party or not
app hostname add|remove <app> <hostname>
app hostname list [<app>] One application's, or the whole estate
app groups show <app>
app groups mode <app> <owned|all>
app groups allow|disallow <app> <group>
Access tokens
token create <service-account> -name <text> -scope <a>,<b> [-for <duration>]
Only a service account: one for a person
would act as them, and they make their
own from the console
token list <login>
token revoke <login> <token-id>
Scopes: identity, applications, profile, decisions, policy, scim,
events. See access tokens.
A <duration> anywhere is a whole number of days (30d) or hours and minutes
(72h, 90m). A value that does not parse names the flag and says what it
takes.
Policy
policy test <file.cedar> [-dsn <url>]
policy publish <file> [-activate]
policy activate <version>
policy list
policy show
policy rule list
policy rule add <kind> <id> [flags] web-access | application-access |
ssh-login | run-as-root
policy rule remove <id>
decisions [<principal>] [-denied] [-limit <n>]
Hosts, POSIX and certificates
host enroll <name> Print the join command for a machine
host credentials <name>
host join -server <url> -token <tok>
host whoami -server <url>
host alias add|remove|list <host> [name]
host acme-credentials <host>
posix assign <user|group> <name>
posix show <user|group> <name>
posix set <user> [-home] [-shell]
posix list
posix adopt <user> <number>
posix adopt -from <report.json,...> [-yes]
ssh [user@]<host> [-l <account>] [-print] [-server <url>] [-auth <flow>]
ssh ca init [-activate] | list | trust | rotate <key-id> [-grace <dur>]
x509 ca init -subject <name> | list | trust | rotate <key-id>
oidc key list | rotate [-grace <dur>]
Events, mail, privacy and diagnosis
ssf stream add <app> -endpoint <url> [-events <a>,<b>] | -delivery poll
ssf stream list | pause <app> | resume <app> | remove <app>
ssf token <app>
ssf status
mail settings | set [flags] | test -to <address> | status
mail templates [-show <kind>] [-reset]
redact <type> <name> [-yes]
audit verify
journal head
journal verify '<fingerprint>'
journal head and journal verify read the journal and nothing else, so a
database role with SELECT on events is enough, and they work with the
server stopped. journal verify exits 0 when the journal still holds the
point, 3 when it was rewritten, 4 when the journal ends before it, and 5 when
retention has dropped it. See the audit journal.
Global
-dsn <url> PostgreSQL connection string, for the commands that open it
(or CARDINAL_DSN; defaults to the local dev database)
-server <url> Where Cardinal is, for the commands that sign in
(or CARDINAL_SERVER)
-auth <flow> `loopback` or `device`. Omitting it works out which can
work here, which is right unless a multiplexer or a remote
desktop makes the guess wrong
Passing -dsn to a command that has moved to the API is refused with the rule
rather than a connection, and the reverse is a flag the command does not
declare. A command is on one path or the other, never both: two
implementations of the same operation means the one nobody reads is the one
that bypasses policy.
The agent
cardinal-agent enroll -server <url> -token <token>
cardinal-agent run -server <url>
cardinal-agent status
cardinal-agent sudoers
cardinal-agent hostcert
cardinal-agent shadow -server <url> [-json] [-users <a>,<b>]
cardinal-agent doctor