@hilbras/keystone 2.6.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +350 -0
- package/README.md +72 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -11
- package/dist/index.js.map +1 -1
- package/dist/plugins/rateLimit.d.ts +10 -7
- package/dist/plugins/rateLimit.d.ts.map +1 -1
- package/dist/plugins/rateLimit.js +75 -36
- package/dist/plugins/rateLimit.js.map +1 -1
- package/dist/routes/admin/organizations.d.ts.map +1 -1
- package/dist/routes/admin/organizations.js +2 -0
- package/dist/routes/admin/organizations.js.map +1 -1
- package/dist/routes/admin/platform.d.ts.map +1 -1
- package/dist/routes/admin/platform.js +14 -1
- package/dist/routes/admin/platform.js.map +1 -1
- package/dist/routes/apiKeys.d.ts.map +1 -1
- package/dist/routes/apiKeys.js +15 -1
- package/dist/routes/apiKeys.js.map +1 -1
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +104 -3
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/emailVerification.d.ts.map +1 -1
- package/dist/routes/emailVerification.js +2 -0
- package/dist/routes/emailVerification.js.map +1 -1
- package/dist/routes/magicLinks.d.ts.map +1 -1
- package/dist/routes/magicLinks.js +2 -0
- package/dist/routes/magicLinks.js.map +1 -1
- package/dist/routes/oauth2.d.ts.map +1 -1
- package/dist/routes/oauth2.js +16 -2
- package/dist/routes/oauth2.js.map +1 -1
- package/dist/routes/password.d.ts.map +1 -1
- package/dist/routes/password.js +2 -0
- package/dist/routes/password.js.map +1 -1
- package/dist/routes/scim.d.ts.map +1 -1
- package/dist/routes/scim.js +2 -0
- package/dist/routes/scim.js.map +1 -1
- package/dist/routes/smsOtp.d.ts.map +1 -1
- package/dist/routes/smsOtp.js +4 -0
- package/dist/routes/smsOtp.js.map +1 -1
- package/dist/routes/totp.d.ts.map +1 -1
- package/dist/routes/totp.js +18 -1
- package/dist/routes/totp.js.map +1 -1
- package/dist/services/configuration/profiles.d.ts +26 -0
- package/dist/services/configuration/profiles.d.ts.map +1 -1
- package/dist/services/configuration/profiles.js +80 -1
- package/dist/services/configuration/profiles.js.map +1 -1
- package/dist/services/events/subscribers/auditLog.d.ts.map +1 -1
- package/dist/services/events/subscribers/auditLog.js +38 -3
- package/dist/services/events/subscribers/auditLog.js.map +1 -1
- package/dist/services/events/types.d.ts +3 -1
- package/dist/services/events/types.d.ts.map +1 -1
- package/dist/services/events/validate.d.ts +1 -0
- package/dist/services/events/validate.d.ts.map +1 -1
- package/dist/services/events/validate.js +5 -1
- package/dist/services/events/validate.js.map +1 -1
- package/dist/services/localRateLimit.d.ts +44 -0
- package/dist/services/localRateLimit.d.ts.map +1 -0
- package/dist/services/localRateLimit.js +86 -0
- package/dist/services/localRateLimit.js.map +1 -0
- package/dist/services/refreshTokenState.d.ts +5 -0
- package/dist/services/refreshTokenState.d.ts.map +1 -0
- package/dist/services/refreshTokenState.js +30 -0
- package/dist/services/refreshTokenState.js.map +1 -0
- package/dist/services/setup/token.d.ts +12 -0
- package/dist/services/setup/token.d.ts.map +1 -1
- package/dist/services/setup/token.js +27 -3
- package/dist/services/setup/token.js.map +1 -1
- package/dist/services/tokens.d.ts +1 -0
- package/dist/services/tokens.d.ts.map +1 -1
- package/dist/services/tokens.js +1 -1
- package/dist/services/tokens.js.map +1 -1
- package/dist/services/trustedProxies.d.ts +24 -0
- package/dist/services/trustedProxies.d.ts.map +1 -1
- package/dist/services/trustedProxies.js +19 -0
- package/dist/services/trustedProxies.js.map +1 -1
- package/dist/services/webhooks.d.ts +16 -0
- package/dist/services/webhooks.d.ts.map +1 -1
- package/dist/services/webhooks.js +48 -3
- package/dist/services/webhooks.js.map +1 -1
- package/dist/setup-server.js +47 -3
- package/dist/setup-server.js.map +1 -1
- package/docs/API-REVIEW.md +121 -0
- package/docs/API.md +457 -0
- package/docs/ARCHITECTURE.md +142 -0
- package/docs/CONTRIBUTING.md +61 -0
- package/docs/DEPLOYMENT.md +257 -0
- package/docs/INTEGRATION.md +336 -0
- package/docs/LOGIN_FORM_INTEGRATION.md +306 -0
- package/docs/MIGRATION-1.7.md +70 -0
- package/docs/MIGRATION-1.8.md +183 -0
- package/docs/MIGRATION-1.9.md +200 -0
- package/docs/MIGRATION-2.0.md +203 -0
- package/docs/MIGRATION-2.4.md +185 -0
- package/docs/PERFORMANCE.md +155 -0
- package/docs/RBAC.md +100 -0
- package/docs/RE-AUDIT.md +72 -0
- package/docs/README.md +54 -0
- package/docs/RELEASE-1.7.md +53 -0
- package/docs/ROADMAP.md +41 -0
- package/docs/SECURITY.md +143 -0
- package/docs/adrs/001-identity-connectors-as-adapters.md +27 -0
- package/docs/adrs/002-versioned-event-bus.md +30 -0
- package/docs/adrs/003-bullmq-for-background-work.md +20 -0
- package/docs/adrs/004-argon2id-password-hashing.md +19 -0
- package/docs/plans/KEYSTONE_IMPROVEMENT_PLAN.md +334 -0
- package/docs/plans/UI_SIMPLIFICATION_IMPROVEMENT_PLAN.md +271 -0
- package/docs/security/audit.md +82 -0
- package/docs/security/configuration.md +60 -0
- package/docs/security/enterprise-sso.md +193 -0
- package/docs/security/mtls.md +132 -0
- package/docs/security/proxy-security.md +128 -0
- package/docs/security/rate-limiting.md +79 -0
- package/docs/security/registry-exceptions.md +34 -0
- package/docs/security/registry.json +649 -0
- package/docs/security/registry.md +657 -0
- package/docs/security/scopes.md +45 -0
- package/docs/security/supply-chain.md +49 -0
- package/docs/security/trust-boundaries.md +111 -0
- package/package.json +15 -6
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# API key scopes and machine principals
|
|
2
|
+
|
|
3
|
+
Covers SEC-023, SEC-024 and SEC-025.
|
|
4
|
+
|
|
5
|
+
## The scope registry is the authority
|
|
6
|
+
|
|
7
|
+
`src/services/scopes.ts` holds every scope the API recognises, who may hold it,
|
|
8
|
+
and which principals are permitted to exercise it. Nothing outside that file
|
|
9
|
+
decides what a credential may do.
|
|
10
|
+
|
|
11
|
+
## There is no wildcard
|
|
12
|
+
|
|
13
|
+
The registry contained a `service_account` entry that any key carrying could
|
|
14
|
+
present against every scope check. A narrowly scoped key therefore behaved as a
|
|
15
|
+
fully privileged one — the scope list on the key was advisory rather than
|
|
16
|
+
binding.
|
|
17
|
+
|
|
18
|
+
It is gone. Every scope must be named, and `hasScopes` compares explicitly.
|
|
19
|
+
|
|
20
|
+
## Enforcement fails closed
|
|
21
|
+
|
|
22
|
+
`requireScopes` returned early when `request.apiKeyId` was absent, so any
|
|
23
|
+
authenticated path reaching a scoped route without going through key
|
|
24
|
+
authentication skipped the check entirely. A missing key is now treated as *no
|
|
25
|
+
authority*, not as *no restriction*.
|
|
26
|
+
|
|
27
|
+
## Some operations are human-only
|
|
28
|
+
|
|
29
|
+
A service account is a machine. It has no password, no second factor and no
|
|
30
|
+
browser, so operations that require a person present are refused to it:
|
|
31
|
+
`profile:*` and `mfa:manage`.
|
|
32
|
+
|
|
33
|
+
The guard is `requireHumanPrincipal` in `src/plugins/machinePrincipal.ts`.
|
|
34
|
+
|
|
35
|
+
> **Placement matters.** It must come **after** `app.authenticate` in a
|
|
36
|
+
> `preHandler`, because that is what populates `request.serviceAccount`. Placed
|
|
37
|
+
> before, it never sees a service account and silently permits everything — the
|
|
38
|
+
> guard appears to work and does nothing.
|
|
39
|
+
|
|
40
|
+
## Migration from 2.4.0
|
|
41
|
+
|
|
42
|
+
Scope names changed in v2.6.0. The `service_account` wildcard no longer exists,
|
|
43
|
+
so a key that relied on it must be reissued with explicit scopes. Keys whose
|
|
44
|
+
scopes are no longer recognised are rejected at creation rather than accepted and
|
|
45
|
+
silently ignored.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Supply chain
|
|
2
|
+
|
|
3
|
+
Covers SEC-011.
|
|
4
|
+
|
|
5
|
+
## What runs on every push and pull request
|
|
6
|
+
|
|
7
|
+
`.github/workflows/supply-chain.yml`:
|
|
8
|
+
|
|
9
|
+
| Job | Tool | Gate |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| OSV vulnerability scan | osv-scanner | fails on any advisory |
|
|
12
|
+
| npm audit | npm | fails at `high` |
|
|
13
|
+
| Dependency review | GitHub action | fails on a new advisory in the diff |
|
|
14
|
+
| SBOM | CycloneDX | artefact, always produced |
|
|
15
|
+
| Container scan | Trivy | image, filesystem and secret scanning |
|
|
16
|
+
| License report | license-checker | fails on an unknown or disallowed licence |
|
|
17
|
+
|
|
18
|
+
`ci.yml` runs the same dependency and licence gates so a branch cannot be green
|
|
19
|
+
locally and red on the merge.
|
|
20
|
+
|
|
21
|
+
## The image finding that no scanner could see
|
|
22
|
+
|
|
23
|
+
The production Dockerfile installed npm so it could prune dev dependencies. That
|
|
24
|
+
npm carried **eight high-severity advisories** — and `npm audit` and the OSV
|
|
25
|
+
scanner both read the lockfile, not the image. Every configured gate was
|
|
26
|
+
satisfied while the shipped artefact was the vulnerable one.
|
|
27
|
+
|
|
28
|
+
npm is removed from the runtime stage. Pruning happens at build time in a stage
|
|
29
|
+
that is not part of the published image.
|
|
30
|
+
|
|
31
|
+
This is worth stating plainly because it is not a scanner gap that more
|
|
32
|
+
scanning would close: the tools were all present, all passing, and all reading a
|
|
33
|
+
different artefact from the one being shipped. Finding it required looking at what
|
|
34
|
+
was actually in the image.
|
|
35
|
+
|
|
36
|
+
## Release gate
|
|
37
|
+
|
|
38
|
+
`release.yml` refuses to publish unless every one of the following holds:
|
|
39
|
+
|
|
40
|
+
- a critical or high advisory exists with no documented exception
|
|
41
|
+
- the security suite passes
|
|
42
|
+
- typecheck, lint and build pass
|
|
43
|
+
- the dependency audit passes
|
|
44
|
+
- secret scanning passes
|
|
45
|
+
- the security registry validates
|
|
46
|
+
- the release metadata is consistent
|
|
47
|
+
|
|
48
|
+
Exceptions live in `docs/security/registry-exceptions.md` and must name the
|
|
49
|
+
advisory, the reason, and an expiry. An undocumented exception fails the gate.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Trust Boundaries
|
|
2
|
+
|
|
3
|
+
Keystone's security model rests on one distinction: **which values a client
|
|
4
|
+
cannot forge**. This document names those values, states where they come from,
|
|
5
|
+
and lists what is deliberately not trusted.
|
|
6
|
+
|
|
7
|
+
## The deployment model
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
┌──────────────────────────────────────────┐
|
|
11
|
+
│ Internet │
|
|
12
|
+
└───────────────────┬──────────────────────┘
|
|
13
|
+
│ attacker-controlled
|
|
14
|
+
┌───────────────────▼──────────────────────┐
|
|
15
|
+
│ Trusted reverse proxy (LB) │
|
|
16
|
+
│ - terminates TLS │
|
|
17
|
+
│ - validates client certificates (mTLS) │
|
|
18
|
+
│ - overwrites forwarded identity headers │
|
|
19
|
+
└───────────────────┬──────────────────────┘
|
|
20
|
+
│ peer address is the proxy's
|
|
21
|
+
┌───────────────────▼──────────────────────┐
|
|
22
|
+
│ Keystone │
|
|
23
|
+
│ trusts forwarded headers ONLY from │
|
|
24
|
+
│ KEYSTONE_TRUSTED_PROXIES │
|
|
25
|
+
└──────────────────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Keystone may also run with no proxy at all. Every rule below holds in both
|
|
29
|
+
layouts; only the trusted-proxy list changes.
|
|
30
|
+
|
|
31
|
+
## Unforgeable values
|
|
32
|
+
|
|
33
|
+
| Value | Source | Trustworthy because |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| Peer address | TCP connection (`socket.remoteAddress`) | Established by the network stack; a client cannot set it |
|
|
36
|
+
| Session cookie / bearer token | Cryptographic check against stored state | Requires a secret the client must already hold |
|
|
37
|
+
| API key | Hashed lookup against a stored secret | Same |
|
|
38
|
+
| Client certificate | Validated by the proxy, forwarded as a fingerprint | Only believed from a trusted peer, and bound to a service account in the database |
|
|
39
|
+
|
|
40
|
+
## Forged values
|
|
41
|
+
|
|
42
|
+
These arrive in request headers and are attacker-controlled unless the peer is a
|
|
43
|
+
configured trusted proxy:
|
|
44
|
+
|
|
45
|
+
- `x-forwarded-for`, `x-real-ip`, `forwarded` — the apparent client address
|
|
46
|
+
- `x-forwarded-client-cert`, `x-client-cert-fingerprint` — the client certificate
|
|
47
|
+
- `x-service-account-id` — a service account identifier
|
|
48
|
+
|
|
49
|
+
When the peer is **not** a trusted proxy, Keystone deletes these headers in an
|
|
50
|
+
`onRequest` hook before routing, authentication, or any handler runs. Stripping
|
|
51
|
+
rather than ignoring them means a route added later cannot read a spoofed
|
|
52
|
+
identity by accident.
|
|
53
|
+
|
|
54
|
+
## Trust decisions and where they are made
|
|
55
|
+
|
|
56
|
+
| Decision | Made in | Rule |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| Is this peer a trusted proxy? | `isFromTrustedProxy()` | Peer address is in `KEYSTONE_TRUSTED_PROXIES` |
|
|
59
|
+
| What is the client address? | `clientAddress()` | Forwarded headers only when the peer is trusted; otherwise the peer |
|
|
60
|
+
| Is this certificate identity real? | `extractClientCert()` | Requires a trusted peer **and** a well-formed SHA-256 fingerprint |
|
|
61
|
+
| Which service account is it? | `resolveByFingerprint()` | The fingerprint is bound to exactly one account in the database |
|
|
62
|
+
|
|
63
|
+
All four live in `src/services/trustedProxies.ts` and `src/plugins/mtls.ts`.
|
|
64
|
+
|
|
65
|
+
## Identity must come from a credential
|
|
66
|
+
|
|
67
|
+
A service account authenticates in one of two ways, and never a third:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
validated client certificate (fingerprint bound to the account)
|
|
71
|
+
OR
|
|
72
|
+
authenticated credential (API key, session, or bearer token)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
There is deliberately **no** header-only path. `x-service-account-id` is accepted
|
|
76
|
+
only as a hint alongside a valid certificate, and only when the account it names
|
|
77
|
+
is the account bound to the presented certificate. A mismatch fails rather than
|
|
78
|
+
falling back — a disagreement is treated as a spoofing signal.
|
|
79
|
+
|
|
80
|
+
## Threat assumptions
|
|
81
|
+
|
|
82
|
+
These are believed, and a deployment that violates them is not supported:
|
|
83
|
+
|
|
84
|
+
1. **The proxy is operated by you.** If an attacker controls a host inside the
|
|
85
|
+
trusted range, they control the identity headers.
|
|
86
|
+
2. **The proxy overwrites, not appends.** It must set `x-forwarded-for` itself.
|
|
87
|
+
A proxy that appends to a client-supplied value lets the client prepend.
|
|
88
|
+
3. **The trusted range is as narrow as the deployment allows.** Trusting a
|
|
89
|
+
whole `0.0.0.0/0` or a large shared-VPC range disables the boundary.
|
|
90
|
+
4. **TLS is terminated before Keystone.** Certificate validity is the proxy's
|
|
91
|
+
responsibility; Keystone only compares a fingerprint it was told about.
|
|
92
|
+
5. **Database access is trusted.** Whoever can write to `service_accounts` can
|
|
93
|
+
bind a certificate to an account.
|
|
94
|
+
|
|
95
|
+
### What is *not* protected here
|
|
96
|
+
|
|
97
|
+
- **Certificate chain validation** is the proxy's job. Keystone verifies that a
|
|
98
|
+
fingerprint is well-formed and bound to an account; it does not re-validate the
|
|
99
|
+
chain or the CA. See [mtls.md](./mtls.md).
|
|
100
|
+
- **A trusted proxy that is compromised** is a full identity bypass by design.
|
|
101
|
+
This is inherent to the mTLS deployment model, not a Keystone defect.
|
|
102
|
+
- **Spoofing against a shared egress IP** cannot be distinguished by peer
|
|
103
|
+
address. Deploy per-service egress ranges if callers share a NAT.
|
|
104
|
+
|
|
105
|
+
## Consequences of a misconfiguration
|
|
106
|
+
|
|
107
|
+
| Misconfiguration | Result |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `KEYSTONE_TRUSTED_PROXIES` unset while behind a proxy | All forwarded headers stripped; rate limiting keys on the proxy's address, so **all** clients share one budget. Clients see rate limits they should not. |
|
|
110
|
+
| Range too broad (e.g. a shared VPC CIDR) | Any host in that range can assert any client identity, including a certificate fingerprint. |
|
|
111
|
+
| Keystone exposed directly to the internet while a proxy range is configured | An attacker's direct request is not from a trusted peer, so it is still safe — but the proxy path is the only one that can carry identity. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hilbras/keystone",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Hilbras Keystone — standalone identity platform: authentication, MFA, authorization, enterprise SSO, SCIM, and token issuance.",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
20
|
"!dist/tests",
|
|
21
|
+
"docs",
|
|
21
22
|
"CHANGELOG.md",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
"dev": "tsx watch src/index.ts",
|
|
29
30
|
"dev:setup": "KEYSTONE_SETUP_MODE=true tsx watch src/setup-server.ts",
|
|
30
31
|
"dev:all": "./start.sh",
|
|
31
|
-
"build": "tsc && npm run copy:migrations",
|
|
32
|
+
"build": "npm run clean && tsc && npm run copy:migrations",
|
|
32
33
|
"build:sdk": "cd packages/keystone-sdk && npm run build",
|
|
33
34
|
"build:all": "npm run build:sdk && npm run build && cd frontend && npm run build",
|
|
34
35
|
"copy:migrations": "mkdir -p dist/db/migrations && cp -r src/db/migrations/* dist/db/migrations/",
|
|
@@ -43,11 +44,17 @@
|
|
|
43
44
|
"db:studio": "drizzle-kit studio",
|
|
44
45
|
"db:seed": "tsx src/db/seed.ts",
|
|
45
46
|
"db:reencrypt-oidc-secrets": "node dist/db/reencryptOidcSecrets.js",
|
|
46
|
-
"test": "node --test --test-force-exit --test-concurrency=1 --test-timeout=60000 dist/tests/*.test.js dist/tests
|
|
47
|
-
"test:security": "node --test --test-force-exit --test-concurrency=1 --test-timeout=60000 dist/tests/security/*.test.js dist/tests/security
|
|
47
|
+
"test": "node --test --test-force-exit --test-concurrency=1 --test-timeout=60000 dist/tests/*.test.js dist/tests/*/*.test.js dist/tests/*/*/*.test.js dist/tests/*/*/*/*.test.js",
|
|
48
|
+
"test:security": "node --test --test-force-exit --test-concurrency=1 --test-timeout=60000 dist/tests/security/*.test.js dist/tests/security/*/*.test.js dist/tests/security/*/*/*.test.js dist/tests/security/*/*/*/*.test.js",
|
|
48
49
|
"test:all": "npm test && cd frontend && npm run test:e2e",
|
|
49
50
|
"setup": "./install.sh",
|
|
50
|
-
"start:all": "./start.sh"
|
|
51
|
+
"start:all": "./start.sh",
|
|
52
|
+
"clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
|
|
53
|
+
"registry:check": "node scripts/verify-security-registry.mjs",
|
|
54
|
+
"registry:render": "node scripts/render-security-registry.mjs",
|
|
55
|
+
"reaudit:check": "node scripts/render-reaudit-matrix.mjs --check",
|
|
56
|
+
"reaudit:render": "node scripts/render-reaudit-matrix.mjs",
|
|
57
|
+
"review:api": "node scripts/review-api-surface.mjs"
|
|
51
58
|
},
|
|
52
59
|
"dependencies": {
|
|
53
60
|
"@authenio/samlify-node-xmllint": "^2.0.0",
|
|
@@ -87,5 +94,7 @@
|
|
|
87
94
|
"reflect-metadata": "^0.2.2",
|
|
88
95
|
"tsx": "^4.15.0",
|
|
89
96
|
"typescript": "^7.0.2"
|
|
90
|
-
}
|
|
97
|
+
},
|
|
98
|
+
"main": "./dist/index.js",
|
|
99
|
+
"types": "./dist/index.d.ts"
|
|
91
100
|
}
|