Skip to content

Commit 51dfe47

Browse files
authored
Merge pull request #149 from drudge/codex/native-passkeys
Add native passkeys and optional password sign-in
2 parents 5be94e1 + 6679db1 commit 51dfe47

55 files changed

Lines changed: 5595 additions & 2710 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@ Create a passphrase-sealed application backup before upgrading and keep
88
mixed-version cluster windows short. Cross-version restore and downgrade
99
compatibility are not yet a published contract.
1010

11+
## [1.3.0]
12+
13+
Sable 1.3.0 adds native passkeys for standalone servers and clusters, making
14+
passwords optional while retaining OpenID Connect and password sign-in.
15+
16+
### Passkeys and account access
17+
18+
- Register and manage passkeys in **Profile → Account**, below Account details.
19+
Sign in without a username using your device's fingerprint, face, PIN, or a
20+
security key. Passkeys require an HTTPS DNS hostname; localhost is supported
21+
for development. Unsupported browsers and ordinary HTTP connections hide
22+
passkey sign-in and registration controls.
23+
- Disable password sign-in after adding a passkey, then re-enable the existing
24+
password without resetting it. Accounts without a password can set one.
25+
Credential removal and password controls use Sable's confirmation dialogs.
26+
- Passkey public credentials replicate and are included in authorization
27+
backups. Existing HTTPS identities and cluster membership determine trusted
28+
origins, with a stable RP ID derived from the registrable domain. Nodes under
29+
the same domain can accept the same credential after replication.
30+
- **Settings → Web → Enable passkeys** controls availability and defaults on.
31+
Disabling preserves saved credentials and blocks passkey authentication and
32+
enrollment. The settings form checks that active accounts have an enabled
33+
password or a link to the enabled OIDC provider before allowing the change.
34+
- The [passkey guide](docs/guides/passkeys.md) covers enrollment, password
35+
recovery, standalone HTTPS, reverse proxies, cluster failover, and backups.
36+
Initial administrator setup still starts with a password.
37+
38+
### Update preferences
39+
40+
- Software Updates in Settings now includes **Include pre-releases** alongside
41+
**Check for updates on sign-in**. Both wait for **Save Settings**.
42+
- The About page retains its release-channel control and shares the same saved,
43+
node-local preference with Settings.
44+
1145
## [1.2.0]
1246

1347
Sable 1.2.0 makes it easier to migrate authoritative zones from Technitium and

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ without shipping a Node.js runtime or a separate frontend bundle.
6262
console navigation
6363
- Database-backed users, built-in/custom RBAC, separate Web/API permissions,
6464
per-zone grants, revocable API tokens, and an AES-256-GCM secret vault
65+
- Native WebAuthn passkeys, passwordless sign-in, and per-account optional
66+
passwords with [passkey management in Profile](docs/guides/passkeys.md)
6567
- OpenID Connect single sign-on with guided setup, PKCE, group-to-role mapping,
6668
just-in-time provisioning, verified-email linking, and replicated federated
6769
identities

config.example.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ tls_insecure = false
208208
# enabled = true
209209

210210
[security]
211+
# Hide passkey sign-in and enrollment, keeping saved credentials.
212+
# Applies locally on standalone servers and replicates in clusters.
213+
# Ensure every account has another sign-in method before setting true.
214+
passkeys_disabled = false
211215
enabled = true
212216
secure_cookies = false
213217
session_ttl = "12h"
@@ -222,7 +226,7 @@ secret_key_file = "data/sable.key"
222226
# Enabling this adds a button to the sign-in page and takes nobody's password
223227
# away. Moving an account to single sign-on only is a per-account switch on the
224228
# Administration page, and Sable keeps one administrator able to sign in with a
225-
# password so a problem at the provider cannot lock everyone out.
229+
# password or passkey so a problem at the provider cannot lock everyone out.
226230
[oidc]
227231
enabled = false
228232
# display_name = "Pocket ID"

docs/clustering.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ Replicated state includes:
7373
- Dynamic DNS settings and external provider credentials;
7474
- UniFi settings and controller credentials;
7575
- OpenID Connect settings, client secret, linked identities, and role mappings;
76-
- users, roles, permission grants, password hashes, API-token hashes, and token
76+
- passkey availability (`security.passkeys_disabled`);
77+
- users, roles, permission grants, password hashes, passkey public credentials, API-token hashes, and token
7778
revocations.
7879

7980
Node-local state includes:
@@ -89,6 +90,11 @@ Replicas validate and activate a complete candidate before recording the new
8990
generation. A rejected candidate leaves the previous runtime and manifest
9091
active.
9192

93+
Passkeys work across trusted node addresses under the same registrable domain.
94+
Credentials replicate, but browser sessions and in-progress sign-in challenges
95+
remain node-local. See [Sign in with passkeys](guides/passkeys.md#hostnames-and-cluster-failover)
96+
for naming and failover examples.
97+
9298
## Monitoring and node queries
9399

94100
The Cluster page refreshes role, connectivity, applied/current generation,

docs/configuration.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,12 +771,33 @@ source compiler statistics, cache entries, and configuration revision.
771771
```toml
772772
[security]
773773
enabled = true
774+
passkeys_disabled = false
774775
secure_cookies = false
775776
session_ttl = "12h"
776777
api_token_ttl = "3mo"
777778
secret_key_file = "data/sable.key"
778779
```
779780

781+
Native passkeys work on standalone servers and clusters without a separate RP ID
782+
or origin list. Enroll them in **Profile → Account → Passkeys** and use **Sign in
783+
with a passkey** on the login page. HTTPS with a DNS hostname is required;
784+
`http://localhost` is supported for development. Enrollment and sign-in controls
785+
are hidden when the browser lacks secure-context or WebAuthn support.
786+
787+
Passwords, OIDC, and passkeys can coexist. In Profile's Password header, disable
788+
password sign-in after enrollment or re-enable the saved password without a
789+
reset. Setting a new password is a separate action that revokes browser sessions.
790+
791+
**Settings → Web → Enable passkeys** controls availability. Click **Save
792+
Settings** to apply it. It defaults on, takes effect without restarting, and
793+
replicates across a cluster. Disabling preserves saved keys and blocks passkey
794+
sign-in and enrollment. The settings form requires an alternate sign-in method
795+
for every active account. Direct TOML edits use `security.passkeys_disabled`;
796+
verify alternate account access before disabling it that way.
797+
798+
See [Sign in with passkeys](guides/passkeys.md) for hostname derivation, proxy
799+
configuration, failover, and recovery.
800+
780801
Security is enabled by default. On the first start, all console routes redirect
781802
to `/setup` until the initial administrator is created. Passwords use Argon2id
782803
with a unique random salt and the OWASP minimum 19 MiB, two-pass profile. Sable
@@ -908,7 +929,7 @@ and ACME provider credentials are handled.
908929
sign-in page; it takes nothing away. Moving an account to SSO only is a
909930
per-account switch under **Administration → the user → Sign-In**, so people opt
910931
in one at a time. Sable refuses to leave the deployment without at least one
911-
administrator who can still sign in with a password, because single sign-on
932+
administrator who can still sign in with a password or passkey, because single sign-on
912933
depends on a service Sable does not run: if the provider is unreachable, its
913934
certificate expires, or a group claim changes shape, that administrator is how
914935
you get in and fix it.

docs/guides/passkeys.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Sign in with passkeys
2+
3+
Passkeys let you sign in to Sable using your device's fingerprint reader, face
4+
recognition, PIN, or a FIDO2 security key, without entering a username or password.
5+
They are enabled by default and work on **standalone servers and clusters**.
6+
Passwords, OpenID Connect (OIDC), and passkeys can all be available together.
7+
8+
Sable stores the credential's public key. The private key stays with your device
9+
or passkey provider. Passkeys use normal Sable browser sessions and account
10+
permissions; they do not replace API tokens.
11+
12+
## Before you begin
13+
14+
- Enable console authentication and complete initial administrator setup with a
15+
password. You can make that password optional after adding a passkey.
16+
- Access Sable through a stable HTTPS DNS hostname with a certificate trusted by
17+
your browser. A standalone server uses its configured HTTPS identities; no
18+
cluster or separate passkey configuration is required.
19+
- Use a browser and authenticator that support passkeys and device verification.
20+
Sable requires a discoverable credential and verification with a fingerprint,
21+
face, device PIN, or security-key PIN.
22+
23+
Ordinary HTTP connections cannot register or use passkeys. The sign-in button,
24+
registration name field, and Add passkey button stay hidden when the browser
25+
lacks a secure context or the WebAuthn API. `http://localhost` is supported for
26+
local development. Access by IP address is not supported for passkey enrollment
27+
or sign-in, even when HTTPS is available.
28+
29+
## Add and use a passkey
30+
31+
1. Sign in with your password or OIDC provider.
32+
2. Open **Profile → Account**. The **Passkeys** card is below Account details and
33+
above Password.
34+
3. Enter a descriptive name, such as “Personal phone,” and select **Add passkey**.
35+
4. Complete your browser's prompt to save the passkey.
36+
5. Sign out and select **Sign in with a passkey** to test it.
37+
38+
Each account can have up to 20 passkeys. The list shows the name, domain,
39+
creation time, and last use. Select **Remove** and confirm in Sable's dialog to
40+
remove a saved credential. Removing it from Sable prevents it from being used
41+
for that account; it does not delete the entry from your device's passkey manager.
42+
43+
On a standalone server, manage passkeys directly on that server. In a cluster,
44+
register and remove them on the primary. Replicas can accept passkey sign-ins
45+
after receiving the credential through replication.
46+
47+
## Disable or re-enable password sign-in
48+
49+
After adding and testing a passkey, select **Disable password sign-in** on the
50+
right side of the **Password** card header and confirm. Your existing password
51+
is retained but cannot be used to sign in. Linked OIDC accounts remain available.
52+
53+
To restore the same password, sign in with a passkey or OIDC and select
54+
**Enable password sign-in** in the Password header. Confirm the change; no
55+
password reset is required.
56+
57+
You can instead fill in New Password and Confirm New Password and select
58+
**Set new password and enable sign-in**. Setting a new password enables password
59+
sign-in and signs out existing browser sessions. An account that never had a
60+
password must set one before it can use password sign-in.
61+
62+
First-run administrator setup and locally created accounts start with a
63+
password. Accounts provisioned through OIDC can add passkeys without ever
64+
setting a password.
65+
66+
Keep a backup passkey or a tested recovery administrator before disabling your
67+
password. Sable refuses to remove your final passkey while password sign-in is
68+
disabled. When disabling a password, it also requires an active administrator
69+
with password or passkey access so recovery does not depend entirely on an
70+
external identity provider. If you lose access to your authenticators, another
71+
administrator can reset your password and enable password sign-in through
72+
Administration.
73+
74+
## Administrator setting
75+
76+
Open **Settings → Web → Passkeys**, change **Enable passkeys**, then select
77+
**Save Settings**. Changing the switch alone does not save it.
78+
79+
Turning it off hides passkey sign-in and enrollment and rejects both new and
80+
in-progress passkey ceremonies. Saved credentials remain in place for later
81+
re-enabling, and they can still be removed from Profile. Password and OIDC
82+
sign-in continue to work when enabled for the account.
83+
84+
The settings form refuses to disable passkeys if an active account has neither
85+
an enabled password nor a link to the currently enabled OIDC issuer. A link to
86+
a different or disabled provider does not count as an alternative.
87+
88+
The setting takes effect without restarting. On a standalone server it applies
89+
to that server; in a cluster it replicates to the other nodes. The configuration
90+
is:
91+
92+
```toml
93+
[security]
94+
enabled = true
95+
passkeys_disabled = false
96+
```
97+
98+
Set `passkeys_disabled = true` to disable the feature. Before editing TOML
99+
directly, ensure every active account has another enabled sign-in method; the
100+
account check described above is performed by the settings form.
101+
102+
## Hostnames and cluster failover
103+
104+
Sable derives the relying party ID (RP ID) from the registrable parent domain of
105+
the hostname you access, using the Public Suffix List. No manual RP ID or
106+
separate allowed-origins list is needed.
107+
108+
| Console address | RP ID |
109+
| --- | --- |
110+
| `https://dns.example.com` | `example.com` |
111+
| `https://ns1.penree.net` | `penree.net` |
112+
| `https://ns2.penree.net` | `penree.net` |
113+
| `http://localhost:5391` | `localhost` |
114+
115+
For a cluster named `ns.penree.net` with nodes `ns1.penree.net` and
116+
`ns2.penree.net`, the RP ID is `penree.net`. The cluster domain itself does not
117+
set the RP ID. Sable handles public suffixes such as `co.uk` and private hosting
118+
suffixes when deriving the registrable domain.
119+
120+
Trusted origins come from existing configuration:
121+
122+
- Advertised HTTPS URLs in the cluster registry and the local advertised URL.
123+
- The HTTPS console's listener hostname and explicit certificate DNS names.
124+
- `encrypted_dns.acme.domains` for ACME, or the explicit names in a manually
125+
installed or generated certificate.
126+
127+
Sable recognizes the configured HTTPS listener port and standard HTTPS port
128+
443. Wildcard certificate names do not authorize arbitrary sibling websites.
129+
Adding or removing a configured identity changes the trusted origins without
130+
maintaining another passkey-specific list.
131+
132+
After replication, a credential registered on `ns1.penree.net` can sign in on
133+
`ns2.penree.net` when both addresses are trusted. Joining a cluster, promoting a
134+
replica, or removing the old primary does not change the RP ID. Browser sessions
135+
and in-progress authentication challenges are node-local: begin a new sign-in
136+
on the replacement node if failover interrupts a login.
137+
138+
Nodes under unrelated registrable domains cannot share a passkey with this
139+
default. Use names under the same domain or retain password/OIDC access. A
140+
credential's RP ID cannot be changed after enrollment; moving to a different
141+
domain requires a new passkey.
142+
143+
## Reverse proxies and backups
144+
145+
A reverse proxy must preserve the original Host and send
146+
`X-Forwarded-Proto: https` to Sable. The browser's origin must match that address.
147+
When no explicit HTTPS identities are configured, such as an HTTP-only backend
148+
behind a proxy, sign-in falls back to the exact verified enrollment origin.
149+
150+
Authorization backups and cluster snapshots include public credentials and user
151+
handles. Restoring them preserves ownership but cannot change their domain
152+
binding or recover a lost private key. The restored server must be reachable at
153+
an authorized HTTPS address under the same domain. Replication preserves newer
154+
local signature counters when applying older credential state.
155+
156+
## Troubleshooting
157+
158+
- **No passkey button:** check the administrator setting, browser support, and
159+
whether you opened an HTTPS hostname or localhost.
160+
- **Hostname rejected:** use a DNS name present in the existing HTTPS identities.
161+
Check the exact hostname and port, certificate names, and proxy headers.
162+
- **Passkey missing on another node:** confirm replication has caught up and the
163+
node uses a trusted address under the same registrable domain.
164+
- **Canceled or timed-out prompt:** retry the sign-in or registration action.
165+
- **Demo passkeys do not work:** the demo's MacBook and iPhone entries are display
166+
fixtures. Their private keys are discarded; keep the demo password enabled or
167+
register your own passkey using localhost.

docs/guides/sso.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# Connect single sign-on
22

33
Let operators use an existing OpenID Connect identity provider while keeping a local recovery account. Enabling SSO adds a sign-in choice; it does not automatically remove local passwords.
4+
[Native Sable passkeys](passkeys.md) can be used alongside OIDC and passwords,
5+
including by OIDC-provisioned accounts that have never set a local password.
46

57
## Before you begin
68

7-
Prepare an OIDC client at your provider, a stable HTTPS console URL, and an administrator who can still sign in with a password. Decide whether unknown users may be provisioned and what, if anything, they receive by default.
9+
Prepare an OIDC client at your provider, a stable HTTPS console URL, and an administrator who can still sign in with a password or passkey. Decide whether unknown users may be provisioned and what, if anything, they receive by default.
810

911
Managing the provider, account provisioning, and role mappings requires both `settings.write` and `users.write`. Use an administrator or a role with both permissions.
1012

@@ -30,7 +32,7 @@ Turn verified-email linking off if your provider cannot reliably verify addresse
3032

3133
Use a second browser session to test a non-administrator user and an intended administrator. Confirm their effective permissions, group mapping, and logout behavior. Keep the local administrator session open until these checks pass.
3234

33-
Switch an account to SSO-only under its **Sign-In** settings only after testing. Sable preserves a password-capable administrator because it cannot repair an unavailable identity provider for you.
35+
Switch an account to SSO-only under its **Sign-In** settings only after testing. Sable requires an administrator with password or passkey sign-in because it cannot repair an unavailable identity provider for you.
3436

3537
## Troubleshoot safely
3638

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ These guides cover Sable 1.0.0. [Download the release](https://github.com/drudge
3131
## Secure it and keep it running
3232

3333
- [Enable encrypted DNS](guides/encrypted-dns.md) and [manage certificates](guides/certificates.md).
34-
- [Grant people the right permissions](guides/access-control.md) or [connect single sign-on](guides/sso.md).
34+
- [Grant people the right permissions](guides/access-control.md), [use passkeys](guides/passkeys.md), or [connect single sign-on](guides/sso.md).
3535
- [Back up and rehearse recovery](backup.md) before your first production upgrade.
3636
- [Build a resilient cluster](clustering.md) and [update nodes safely](guides/updates.md).
3737
- [Keep a public name pointed at your connection](guides/dynamic-dns.md), [sync UniFi device names](guides/unifi.md), or [add a Glance widget](guides/glance.md).

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ toolchain go1.27.1
77
require (
88
github.com/a-h/templ v0.3.1020
99
github.com/fsnotify/fsnotify v1.10.1
10+
github.com/fxamacker/cbor/v2 v2.9.3
11+
github.com/go-webauthn/webauthn v0.18.1
1012
github.com/jackc/pgx/v5 v5.11.0
1113
github.com/miekg/dns v1.1.73
1214
github.com/pelletier/go-toml/v2 v2.4.3
@@ -27,6 +29,10 @@ require (
2729
github.com/cli/browser v1.3.0 // indirect
2830
github.com/dustin/go-humanize v1.0.1 // indirect
2931
github.com/fatih/color v1.19.0 // indirect
32+
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
33+
github.com/go-webauthn/x v0.3.1 // indirect
34+
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
35+
github.com/google/go-tpm v0.9.8 // indirect
3036
github.com/google/uuid v1.6.0 // indirect
3137
github.com/jackc/pgpassfile v1.0.0 // indirect
3238
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -36,7 +42,10 @@ require (
3642
github.com/mattn/go-isatty v0.0.24 // indirect
3743
github.com/natefinch/atomic v1.0.1 // indirect
3844
github.com/ncruces/go-strftime v1.0.0 // indirect
45+
github.com/philhofer/fwd v1.2.0 // indirect
3946
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
47+
github.com/tinylib/msgp v1.6.4 // indirect
48+
github.com/x448/float16 v0.8.4 // indirect
4049
golang.org/x/sync v0.23.0 // indirect
4150
golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 // indirect
4251
golang.org/x/text v0.42.0 // indirect

0 commit comments

Comments
 (0)