siftp

Admin console

The admin listener serves a web console next to the REST API and MCP. You sign in with a passkey; there are no passwords. Everything the console does goes through the same service layer as the API and MCP, so the audit log and error objects are the same.

SectionWhat it shows or does
StorageDisk usage of [server].root, tiered-storage state, per-user usage against quota
UsersCreate, edit, and delete API-managed users; set passwords
SessionsLive FTP, FTPS, and SFTP sessions; end one
LogsThe server log and the audit log, with filters; audit chain check
AdminsInvite and remove admins
AccountYour passkeys, personal access tokens, and connected apps
Connect an LLMThe MCP URL and how to sign a client in (Agents and MCP)

Turn it on

The console needs an origin, which is a URL whose host the browser trusts for passkeys.

On the server itself. A loopback bind needs no extra settings. The console is at http://localhost:<port>:

[admin]
bind = "127.0.0.1:8080"

Reach it from your workstation with a port forward. Any local port works, so pick a free one:

ssh -L 8080:127.0.0.1:8080 files.example.com
aws ssm start-session --target i-0123456789abcdef0 \
  --document-name AWS-StartPortForwardingSession \
  --parameters '{"portNumber":["8080"],"localPortNumber":["8080"]}'

Open http://localhost:8080. A request by IP address (127.0.0.1) is redirected to localhost, because passkeys cannot use IP addresses.

On a public host.

[admin]
bind = "0.0.0.0:443"
tls = true                                  # uses the [ftps] certificate
public_url = "https://files.example.com"    # a DNS name, never an IP address
SettingWhy
tls = trueServes HTTPS with the [ftps] certificate. The certificate must name the host in public_url
public_urlThe console origin. Its host is the passkey relying-party id and the OAuth issuer
DNS namePasskeys cannot be bound to an IP address

With a non-loopback bind and no public_url, the console is off and only the bearer API is served. siftpd --check-config says so.

Changing the host in public_url invalidates every passkey. Recover each admin with admin-invite --recover (below).

[admin].ui = false turns the console off and keeps the API.

First admin

An invite link creates the first admin. Run this on the server:

sudo -u siftp siftpd admin-invite alice
{"admin":"alice","recover":false,"expires_unix":1761700000,"url":"https://files.example.com/admin/#/invite/..."}

Open the URL, then register a passkey. The link is valid for 24 hours and works once. It is stored as a hash, so a lost link cannot be shown again: run the command again, and the new link replaces the old one. (In the console, a second invite for a pending name is refused until the first is revoked.)

Run it as the siftp user, not as root. The command writes the accounts file, and a root-owned file is one the daemon cannot read. The command checks the owner of the accounts file's directory and refuses to run as anyone else.

Recovery

An admin who lost every passkey, or whose host changed, gets a new link:

sudo -u siftp siftpd admin-invite alice --recover

The link adds a passkey to the existing admin alice. Without --recover, an invite for a name that already exists is refused, and --recover for an unknown name returns not_found.

More admins

Any signed-in admin can invite another from Admins. The link is shown once. Removing an admin ends their sessions, tokens, connected apps, and pending invites. The last admin cannot be removed.

Sessions

LimitValue
Idle30 minutes
Absolute12 hours (2 hours on http://localhost)
RestartSigns everyone out; sessions are in memory

Sessions are cookies bound to the console origin: HttpOnly, SameSite=Strict, and __Host- prefixed on HTTPS. Changes need the console's exact Origin header.

The fresh passkey check

These actions need a passkey assertion from the last 5 minutes. A stale session gets 403 with X-Siftp-Step-Up: required, and the console asks for your passkey again.

ActionWhy
Invite or remove an adminHands out or takes away access
Add or remove a passkeyA stolen session could add its own passkey and outlast the session
Create a personal access tokenA token outlives the session
Approve an OAuth clientGives an outside service access

An admin's last passkey cannot be removed. Removing a passkey ends the sessions it signed in.

Personal access tokens

Create one in Account. It is shown once and looks like siftp_pat_<id>_<secret>. Only its SHA-256 is stored.

FieldValues
Scopesiftp:read (GET routes and read-only MCP tools, the default) or siftp:admin (also changes)
Lifetime1 to 365 days (default 90)

Use it as a bearer token on /api/v1/*, /metrics, and /mcp:

curl -H "Authorization: Bearer $SIFTP_PAT" https://files.example.com/api/v1/storage

Tokens are the way for scripts and Prometheus to reach a public console. [admin].token still works, but only from loopback peers while the console is public. A wrong or expired token returns 401 and does not count toward login bans: tokens are 256-bit secrets, and an LLM provider's shared address must not get banned.

What tokens and LLM clients cannot do

Only a passkey session manages credentials. A personal access token, an OAuth token, and [admin].token get permission_denied for:

No credential can set a user's root to a directory that holds siftp's own files (config, accounts file, user store, revoked-key list, audit log, TLS and host keys), or one containing ... No MCP tool manages admins or credentials.

The accounts file

[admin].accounts_file (default /var/lib/siftp/admins.toml, mode 0600) holds admins, passkey public keys, invites, tokens, and OAuth grants. Codes and tokens are stored only as SHA-256 hashes.

Audit actor

Admin changes (users, passwords, sessions, reload, and console accounts) carry an actor field. remote is the caller's address.

{"ts":1761600000,"user":"erin","action":"USER_CREATE","path":"","result":"ok","remote":"203.0.113.9:50122","actor":"ui:alice","seq":97,"chain":"4c1a…"}
actorWho acted
ui:<admin>A passkey session in the console
pat:<admin>/<token name>A personal access token
oauth:<admin>/<client host>An OAuth client, such as oauth:alice/claude.ai
token[admin].token
stdiosiftpd mcp-stdio
clisiftpd admin-invite; for a new admin, the creator (ui:<admin> or cli)

Console sign-ins are ADMIN_LOGIN; a wrong [admin].token is ADMIN_AUTH. SPEC §8.5 lists every action. admin-invite itself writes no audit line (the chain has one writer). The invite appears as ADMIN_CREATE or PASSKEY_ADD with actor cli when it is used.

See also