> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tofunmiadewuyi.com/custos/llms.txt
> Use this file to discover all available pages before exploring further.

# Grant SSH access

> Register a public key, grant host.access, and verify the login path.

Two steps: the user's public key has to exist in Custos, and they need an active `host.access` grant
on the host.

## 1. Register the user's SSH public key

```bash theme={null}
curl -sS -X POST "$CUSTOS_URL/keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"laptop","public_key":"ssh-ed25519 AAAA... you@laptop"}'
```

Keys belong to the user, not to a host. `GET /keys` lists them; `DELETE /keys/{id}` removes one.
Deleting a key does not erase its history — the fingerprint is denormalized into the SSH access log.

## 2. Grant `host.access`

```bash theme={null}
curl -sS -X POST "$CUSTOS_URL/grants" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"...","permission":"host.access","target_kind":"host","target_id":"..."}'
```

Target a group instead of a host (`"target_kind":"group"`) to grant access to every host in that
group, including hosts added later.

The grant recomputes the host's snapshot and pushes it over the daemon's live WebSocket
immediately. Offline hosts reconcile on their next connect.

## 3. Log in

```bash theme={null}
ssh deploy@app-01
```

The account must be one of the host's declared accounts. sshd calls `custosd authkeys`, which asks
the daemon for the authorized keys for that account.

## Revoking

`DELETE /grants/{id}` revokes the grant and pushes a fresh snapshot — effective on the next login
attempt, not just the next reboot. Revocation is a soft delete, so the trail survives.

To cut a person off everywhere at once, suspend the user
(`POST /users/{id}/suspend`): that kills their sessions and pushes new snapshots to every affected
host. The snapshot query filters on active user status, which is what makes suspension cut SSH as
well as API access.

## Troubleshooting

**A connected daemon reports `cached ssh keys: 0`.** Force a resync:

```bash theme={null}
curl -sS -X POST "$CUSTOS_URL/hosts/$HOST_ID/refresh" \
  -H "Authorization: Bearer $ADMIN_TOKEN"
```

The response includes `key_count`. If that is `0`, the control plane itself has no active
`host.access` grant plus matching public key for the host — the problem is the grant or the key, not
delivery.

**Logins fail while the control plane is down.** They should not. `custosd authkeys` retries the
daemon socket and then falls back to the last-known-good cache file on disk.

**sshd is not calling Custos at all.** Check the drop-in exists and sshd accepted it:

```bash theme={null}
cat /etc/ssh/sshd_config.d/70-custos.conf
sudo sshd -t
```

`AuthorizedKeysCommandUser` must be a real existing user.

## Auditing

| Endpoint | Shows |
| - | - |
| `GET /hosts/{id}/ssh-audit` | SSH logins against the host |
| `GET /hosts/{id}/access-audit` | access changes affecting the host |
| `GET /grant-audit` | grant and revoke history (admin) |
| `GET /audit` | the most recent activity across the install (admin) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.