DocsReferenceCardinal 0.7.0

Command reference

Every command in both binaries, grouped as they group themselves, and which of them still opens the database.

llms.txt

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 app subcommand
  • POSIX identity: every posix subcommand, including posix adopt
  • Access tokens: token create, list and revoke
  • Reading the directory: list and show
  • Notification email: every mail subcommand
  • ssh, host join and host whoami
  • This terminal's session: status and logout

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