# 2. Add authentication and repository roles

Previous: [minimal server](/docs/tutorials/01-minimal-server.md) · [All tutorials](/docs/tutorials.md) · Next: [gateway scaling](/docs/tutorials/03-add-gateway.md)

You will put a shared auth service in front of the existing `game` repository,
switch storage to TLS, and give Alice and Bob real identities and roles. One
`scribe-ctl add auth` does the provisioning; you then add the people and
grants. Run the commands from the `scribe-demo` directory of stage 1.

## 1. Finish plaintext work before the switch

The cluster should be stopped after stage 1. If you have continued working,
first finish or resolve pending submits while it still runs in plaintext,
inspect `scribe status --scan` and `scribe lock list` in both working copies,
and release held locks. Preserve any unsubmitted or untracked files separately.
Then stop it with `scribe-ctl --config scribe-cluster.toml down`.

Take an offline backup into a new destination:

```sh
scribe-server backup --data-dir ./data/storage-a --to ./backup-before-auth
```

Enabling auth preserves accepted history, but existing plaintext workspaces have
no authenticated owner: they still sync, but submitting needs a new working
copy made after logging in. This stage creates **new working copies** and keeps
`work/alice-plain` and `work/bob-plain` intact for comparison and recovery. Do
not copy their `.scribe/_state` directory into the new copies; reapply any
preserved local edits explicitly after syncing the new copies.

## 2. Add auth to the cluster

Look at the plan. It shows how the definition changes and every step:

```sh
scribe-ctl --config scribe-cluster.toml add auth --plan
```

```text
Scribe · local · add auth plan · auth variant · native runtime
  ...
  ENDPOINTS
    auth       tls://127.0.0.1:7443
    storage-a  tls://127.0.0.1:7447

  CHECKS
    ok    definition    add auth rewrites scribe-cluster.toml with auth
    ...
  DEFINITION CHANGES
    @ scribe-cluster.toml
      [clusters.local]
      # What setup builds: storage, auth or gateway. Setup refuses services that do not match it.
    - variant = "storage"
    + variant = "auth"
    ...
    + # Accounts, credentials and access decisions. TLS only.
    + [clusters.local.auth]
    + # A fixed port: credentials name this address.
    + listen = "127.0.0.1:7443"
    + data_dir = "data/auth"
    + # Written by setup and needed by every auth start. Back it up apart from the data.
    + pepper_file = "secrets/auth.pepper"
    + # Revision witness: keep it outside the data directory and its backups.
    + revision_file = "secrets/auth.revision"
    + # The bootstrap administrator's credential, written by setup. Keep it private.
    + admin_credential = "secrets/admin.credential"
    ...
    - auth = false
    + auth = true
    + # The auth namespace this server's repositories belong to.
    + namespace = "studio"
    + # This server's own credential, issued by setup. Keep it private.
    + credential = "secrets/storage-a.credential"
    ...
```

Then apply it and start the cluster:

```sh
scribe-ctl --config scribe-cluster.toml add auth
scribe-ctl --config scribe-cluster.toml up
```

```text
Scribe · local · Auth added
  rewrote scribe-cluster.toml; the previous definition is kept as scribe-cluster.toml.bak
  created the storage and gateway CA in pki/ca
  created the auth CA in pki/auth-ca
  issued pki/auth/server.pem
  issued pki/storage-a/server.pem
  initialized data/auth; administrator credential secrets/admin.credential
  storage-a: bound to namespace studio
  server storage-a: registered in studio
  storage-a: credential issued to secrets/storage-a.credential
  scribe-monitor: created with its grants in studio
  scribe-monitor: credential issued to secrets/monitor.credential
  wrote CLUSTER-INFO.txt

  ENDPOINTS
    auth       tls://127.0.0.1:7443
    storage-a  tls://127.0.0.1:7447

  AFTERWARDS
    - Start it with `scribe-ctl --config /home/you/scribe-demo/scribe-cluster.toml up`: auth first, then storage.
    - Clients switch from tcp:// to tls://127.0.0.1:7447 (trusting pki/ca/ca.pem with --tls-ca) and log in through auth at tls://127.0.0.1:7443 (trusting pki/auth-ca/ca.pem with --auth-ca); plaintext connections are refused from now on.
    - Nobody has access yet: add people and grants with scribe-auth and the administrator credential secrets/admin.credential (the Next commands in CLUSTER-INFO.txt).
    - Workspaces created before auth have no signed-in owner: they still sync, but submitting needs a new workspace (scribe workspace create) made after logging in; bring unsubmitted work over by hand.
    - Give clients pki/ca/ca.pem and pki/auth-ca/ca.pem; keep the CA private keys (ca-key.pem beside them) offline.
  ...
Scribe · local · running
  SERVICE    STATE        PID  ENDPOINT
  auth       running    17165  tls://127.0.0.1:7443
  storage-a  running    17166  tls://127.0.0.1:7447
```

`up` starts auth first, then storage. The `game` repository and its history
are unchanged; storage now serves only TLS with sign-in.

### What scribe-ctl did for you

- **Two certificate authorities.** Servers prove who they are with TLS
  certificates. `pki/ca` signs the storage (and later gateway) certificates;
  `pki/auth-ca` signs only auth's, so no storage or gateway key can ever pass
  as auth. Clients get the two public `ca.pem` files; the private
  `ca-key.pem` files are needed only to issue and renew server certificates
  and to rotate a CA: move them offline once you have what you need, and give
  their directory to `scribe-ctl renew --ca-dir` (for `pki/auth-ca`,
  `--auth-ca-dir`) when a certificate is due. These are server certificates only:
  people and servers sign in with **credentials** auth issues from its own
  private CAs (see [identity](/docs/auth.md#identity-device-keys-and-certificates)).
- **The auth database** in `data/auth`, created by `scribe-auth init`, with two
  files kept **outside** it:

  | File | Who reads it | What to keep in mind |
  | --- | --- | --- |
  | `secrets/auth.pepper` | Auth, at every start | The password pepper; back it up apart from `data/auth`: without it no password can be checked |
  | `secrets/auth.revision` | Auth, at every start | The revision witness: auth refuses to start from a database older than it, so a restored old copy cannot revive revoked credentials. Keep it out of `data/auth`'s backups |
  | `secrets/admin.credential` | You, for `scribe-auth` administration | The nonexpiring bootstrap administrator credential: auth's address, the auth CA, a device key and its certificate. There is no administrator password; never give this file to users |

- **The store joined a namespace.** `scribe-server auth-init` bound
  `data/storage-a` to the auth namespace `studio` in place (a bound store no
  longer starts without auth), and auth registered `storage-a` as a server of
  `studio`.
- **Server credentials.** `secrets/storage-a.credential` is the store's own
  identity towards auth (valid 365 days); it can check callers but cannot
  administer anything. `secrets/monitor.credential` belongs to the
  `scribe-monitor` service account, a namespace reader that `scribe-ctl df` and
  `repo list` use to read inventory. [Stage 4](/docs/tutorials/04-use-ctl.md#5-renew-credentials-and-certificates)
  renews both before they expire.

On Unix every credential file is written with mode 0600, and auth stores only a
digest of each public key: private keys stay with their holders. The same
steps by hand are [bare metal, stage 2](/docs/manual/bare-metal.md#2-add-authentication)
and [Docker Compose, stage 2](/docs/manual/docker-compose.md#2-add-authentication); the
[certificates guide](/docs/manual/certificates.md#the-four-certificate-authorities)
shows all four CAs, these two and auth's internal ones.

## 3. Check the new endpoints

```sh
scribe-ctl --config scribe-cluster.toml status
scribe --server tls://127.0.0.1:7447 --tls-ca ./pki/ca/ca.pem repo list
```

`status` shows both services with `tls://` endpoints. The `repo list`, made
without signing in, prints `(no repositories)`: `game` is still there, but
nobody can see it until you add people and grants. A `tcp://` connection is
refused.

## 4. Connect the auth administration CLI and add users

Save this as `admin-env.sh` in the deployment directory:

```sh
export SCRIBE_DEMO="$PWD"
auth() {
  scribe-auth --admin-credential "$SCRIBE_DEMO/secrets/admin.credential" \
    --state-dir "$SCRIBE_DEMO/admin-state" "$@"
}
```

The credential file names the auth endpoint and its CA, so no other connection
options are needed. Load the helper and verify administrator access:

```sh
. ./admin-env.sh
auth users list
```

```text
users/admin	fb162b07d0f4ee236b451a917262ec58	admin	true	true	person
users/scribe-monitor	9c4bdb7a5d6ab38a6afeee3ea99741cd	scribe-monitor	true	false	service
```

This `auth` function is only a short wrapper around the **`scribe-auth` CLI**;
it is not `scribe login`. It can be loaded again in any terminal from the
deployment directory. `admin-state` holds durable pending requests, so an
interrupted administrative command can be retried with its original intent.

Create the password files empty and private first, so a password is never
written to a file others can read:

```sh
mkdir -p passwords
(umask 077 && touch ./passwords/alice ./passwords/bob)
ls -l ./passwords   # -rw------- each
```

Then open each in a text editor and enter a different password of 12–1024 UTF-8
bytes; a trailing newline is allowed. Most editors keep an existing file's mode;
repeat the `ls -l` afterwards to confirm. Then create the users:

```sh
auth users add alice
auth users password alice --password-file ./passwords/alice
auth users add bob
auth users password bob --password-file ./passwords/bob
auth users list
```

`users add` creates the identity; `users password` sets its login password. The
same password command later resets it and invalidates the user's existing
sessions. Neither user is an auth administrator, and neither receives a
credential file: people sign in with `scribe login`, which creates a device key
on their own machine.

## 5. Assign responsibilities

Authentication answers who someone is; grants decide what they may do. In this
exercise Alice manages the `studio` namespace and Bob starts as a reader of `game`:

- An **auth administrator** uses an administrator credential to manage users,
  credentials, server identities, and grants. That status alone gives no
  repository access.
- A **namespace administrator** can create repositories and has all repository
  permissions within that namespace.
- A repository **reader** can discover, inspect, and sync its history. A
  **writer** can also submit, branch, and take their own locks. A **maintainer**
  can additionally force-unlock and change the main branch's lock policy.

See the [complete role model](/docs/auth.md#credentials-and-roles) for the exact
permissions.

```sh
auth namespace-admins set alice studio
```

## 6. Log in as Alice and give Bob repository access

Save `client-env.sh` in the deployment directory:

```sh
export SCRIBE_DEMO="$PWD"
export SCRIBE_ENDPOINT=tls://127.0.0.1:7447
alice() {
  scribe --config-dir "$SCRIBE_DEMO/clients/alice" \
    --tls-ca "$SCRIBE_DEMO/pki/ca/ca.pem" --server "$SCRIBE_ENDPOINT" "$@"
}
bob() {
  scribe --config-dir "$SCRIBE_DEMO/clients/bob" \
    --tls-ca "$SCRIBE_DEMO/pki/ca/ca.pem" --server "$SCRIBE_ENDPOINT" "$@"
}
```

Load both helpers and log in:

```sh
. ./admin-env.sh
. ./client-env.sh
alice login alice --auth tls://127.0.0.1:7443 --auth-ca ./pki/auth-ca/ca.pem
alice repo info game
```

```text
Logged in as alice; session valid for 90 days.
info: game id=689a965e479c1decf32f3c9ca7e3f913 head=2 main=2403ffcaa1d1 storage-compression=lz4
```

Enter Alice's password at the hidden prompt (for automation, add
`--password-file ./passwords/alice`). `login` creates a device key on this
machine and sends auth only its public key with the password; auth answers
with a certificate for the session (90 days by default). Before talking to
storage, the client obtains a short access certificate (one hour) from auth and
renews it on its own. The explicit CA files verify both endpoints: `--tls-ca`
the storage CA, `--auth-ca` the auth CA. An ordinary user can instead approve
the administrator-provided fingerprint using
[remembered trust](/docs/client-usage.md#trust-a-server). Sessions are saved
under `clients/alice` and `clients/bob` here; normal installations use the
[per-user configuration root](/docs/reference/cli/scribe.md#defaults-and-configuration).

Copy the **full repository ID** from `repo info game` and substitute it for
`REPOSITORY_ID` below, without angle brackets. Grants use `namespace/id`, not
`studio/game`:

```sh
auth grants set bob studio/REPOSITORY_ID --role reader
bob login bob --auth tls://127.0.0.1:7443 --auth-ca ./pki/auth-ca/ca.pem
alice workspace create game ./work/alice
bob workspace create game ./work/bob
(
  cd work/alice
  alice sync
)
(
  cd work/bob
  bob sync
  cat Assets/Hero.txt
  bob edit Assets/Hero.txt
  printf 'hero mesh v3\n' > Assets/Hero.txt
  bob submit -m "Authenticated hero update"
)
```

Both authenticated copies contain `hero mesh v2`. Bob can read, but a reader
cannot submit:

```text
scribe: permission denied; submit needs the writer role in repository 'game'; ask a repository administrator to grant it (forbidden)
```

Change his existing grant to writer and submit the same edit again:

```sh
auth grants set bob studio/REPOSITORY_ID --role writer
(
  cd work/bob
  bob submit -m "Authenticated hero update"
)
(
  cd work/alice
  alice sync
  alice log
)
```

```text
submitted change 3 (1 file, 1 of 1 chunks sent, 208 object bytes sent for 13 logical bytes, 2 server flushes)
...
change 3  branch main  2026-09-27T12:44:09.034Z  bob  1 entry
    Authenticated hero update
change 2  branch main  2026-09-27T12:43:33.029Z  bob  1 entry
    Hero v2
change 1  branch main  2026-09-27T12:43:32.472Z  alice  1 entry
    Initial hero
```

The new change is attributed to `bob`; `--author` cannot impersonate another
authenticated user. A grant update can take up to 30 seconds to affect new
admissions on existing connections; retry after that bound if needed.

## 7. See who can do what

```sh
scribe-ctl --config scribe-cluster.toml access
```

```text
Scribe · local · access
  ACCOUNT         KIND     STATE    SCOPE                                               ACCESS          DETAIL
  admin           person   enabled  auth                                                administrator   accounts, grants and credentials
  admin           person   enabled  credential bootstrap                                user            never expires
  alice           person   enabled  namespace studio                                    administrator   every repository; creates repositories
  alice           person   enabled  sessions                                            1 signed in     latest expires 2026-12-26
  bob             person   enabled  repository studio/689a965e479c1decf32f3c9ca7e3f913  writer
  bob             person   enabled  sessions                                            1 signed in     latest expires 2026-12-26
  scribe-monitor  service  enabled  namespace studio                                    reader          every repository, including later ones
  scribe-monitor  service  enabled  credential scribe-ctl                               service-reader  expires 2027-09-27
```

Each privilege is its own row: `admin` administers auth (no repository
access), `alice` administers `studio`, `bob` writes to one repository, and
`scribe-monitor` only reads. `access --user bob` narrows it to one account.
Server credentials such as `storage-a`'s are not listed there; `auth
credentials list` shows every credential.

You now have a secure single-storage deployment. Keep it running for
[3. Scale with a gateway](/docs/tutorials/03-add-gateway.md), or stop it with
`scribe-ctl --config scribe-cluster.toml down`.
