@lamalibre/create-lamaste 4.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/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
import { getConfig } from '../lib/config.js';
|
|
3
|
+
import {
|
|
4
|
+
REFRESH_THRESHOLD,
|
|
5
|
+
validateUserSession,
|
|
6
|
+
refreshUserSession,
|
|
7
|
+
} from '../lib/user-access-session.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fastify plugin that validates user-access session tokens from the
|
|
11
|
+
* Authorization: Bearer header. Sets request.userAccessUsername on success.
|
|
12
|
+
*
|
|
13
|
+
* Refreshed tokens are returned via X-User-Session response header so the
|
|
14
|
+
* desktop client can update its stored token.
|
|
15
|
+
*/
|
|
16
|
+
async function userAccessSessionPlugin(fastify, _opts) {
|
|
17
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
const sessionSecret = config.sessionSecret;
|
|
20
|
+
|
|
21
|
+
if (!sessionSecret) {
|
|
22
|
+
return reply.code(401).send({ error: 'user_session_required' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Extract Bearer token from Authorization header
|
|
26
|
+
const authHeader = request.headers.authorization;
|
|
27
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
28
|
+
return reply.code(401).send({ error: 'user_session_required' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tokenValue = authHeader.slice(7);
|
|
32
|
+
const result = validateUserSession(tokenValue, sessionSecret, {
|
|
33
|
+
userEpochs: config.userEpochs ?? {},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!result.valid) {
|
|
37
|
+
return reply.code(401).send({ error: 'user_session_required' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set username on request for downstream route handlers
|
|
41
|
+
request.userAccessUsername = result.payload.username;
|
|
42
|
+
|
|
43
|
+
// Throttle refresh: only re-sign if lastActivity is stale (>60s)
|
|
44
|
+
const now = Math.floor(Date.now() / 1000);
|
|
45
|
+
if (now - result.payload.lastActivity >= REFRESH_THRESHOLD) {
|
|
46
|
+
const refreshed = refreshUserSession(result.payload, sessionSecret);
|
|
47
|
+
reply.header('X-User-Session', refreshed.value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default fp(userAccessSessionPlugin, { name: 'user-access-session' });
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { validateAndConsumeToken, lookupToken } from '../lib/enrollment.js';
|
|
3
|
+
import { signCSR } from '../lib/csr-signing.js';
|
|
4
|
+
import { fingerprintCertPem } from '../lib/mtls.js';
|
|
5
|
+
|
|
6
|
+
const EnrollBodySchema = z.object({
|
|
7
|
+
token: z.string().min(1, 'Enrollment token is required'),
|
|
8
|
+
csr: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1, 'CSR is required')
|
|
11
|
+
.refine((v) => v.includes('BEGIN CERTIFICATE REQUEST'), {
|
|
12
|
+
message: 'CSR must be PEM-encoded',
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const LookupBodySchema = z.object({
|
|
17
|
+
token: z.string().min(1, 'Enrollment token is required'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Public enrollment routes (no mTLS required).
|
|
22
|
+
*
|
|
23
|
+
* The agent doesn't have a cert yet — the one-time token (single-use,
|
|
24
|
+
* 10-minute expiry) is the sole authentication gate.
|
|
25
|
+
*/
|
|
26
|
+
export default async function enrollmentRoutes(fastify, _opts) {
|
|
27
|
+
// ------------------------------------------------------------------
|
|
28
|
+
// POST /lookup — return the expected label for a token without
|
|
29
|
+
// consuming it. Required because the panel's CSR signing wrapper no
|
|
30
|
+
// longer overrides the CSR subject — the agent must build a CSR with
|
|
31
|
+
// the correct CN before submitting it for signing.
|
|
32
|
+
// ------------------------------------------------------------------
|
|
33
|
+
fastify.post('/lookup', async (request, reply) => {
|
|
34
|
+
const body = LookupBodySchema.parse(request.body);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const result = await lookupToken(body.token);
|
|
38
|
+
return { ok: true, label: result.label, type: result.type };
|
|
39
|
+
} catch (err) {
|
|
40
|
+
const statusCode = err.statusCode || 401;
|
|
41
|
+
return reply.code(statusCode).send({
|
|
42
|
+
error: err.message || 'Invalid enrollment token',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ------------------------------------------------------------------
|
|
48
|
+
// POST / — enroll an agent using a one-time token + CSR
|
|
49
|
+
// ------------------------------------------------------------------
|
|
50
|
+
fastify.post('/', async (request, reply) => {
|
|
51
|
+
const body = EnrollBodySchema.parse(request.body);
|
|
52
|
+
|
|
53
|
+
let tokenData;
|
|
54
|
+
try {
|
|
55
|
+
tokenData = await validateAndConsumeToken(body.token);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const statusCode = err.statusCode || 401;
|
|
58
|
+
return reply.code(statusCode).send({
|
|
59
|
+
error: err.message || 'Invalid enrollment token',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Build opts for delegated enrollments
|
|
65
|
+
const signOpts =
|
|
66
|
+
tokenData.type === 'delegated'
|
|
67
|
+
? { type: /** @type {const} */ ('delegated'), delegatedBy: tokenData.delegatedBy }
|
|
68
|
+
: undefined;
|
|
69
|
+
|
|
70
|
+
const result = await signCSR(
|
|
71
|
+
body.csr,
|
|
72
|
+
tokenData.label,
|
|
73
|
+
tokenData.capabilities,
|
|
74
|
+
tokenData.allowedSites,
|
|
75
|
+
request.log,
|
|
76
|
+
signOpts,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Audit: enrollment is the moment a fresh agent cert enters circulation.
|
|
80
|
+
// The token holder is the de facto principal — there is no client cert at
|
|
81
|
+
// this point. Token type ('p12'/'delegated') and delegating label
|
|
82
|
+
// (when present) capture the chain of trust.
|
|
83
|
+
request.log.info(
|
|
84
|
+
{
|
|
85
|
+
admin:
|
|
86
|
+
tokenData.type === 'delegated'
|
|
87
|
+
? `agent:${tokenData.delegatedBy ?? 'unknown'}`
|
|
88
|
+
: 'enrollment-token',
|
|
89
|
+
issuedFor: tokenData.type === 'delegated' ? result.label : `agent:${result.label}`,
|
|
90
|
+
mode: tokenData.type === 'delegated' ? 'delegated' : 'hardware-bound',
|
|
91
|
+
newSerial: result.serial,
|
|
92
|
+
fingerprintSha256: result.certPem ? fingerprintCertPem(result.certPem) : null,
|
|
93
|
+
},
|
|
94
|
+
'mTLS certificate issued (agent enrollment)',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
cert: result.certPem,
|
|
100
|
+
caCert: result.caCertPem,
|
|
101
|
+
label: result.label,
|
|
102
|
+
serial: result.serial,
|
|
103
|
+
expiresAt: result.expiresAt,
|
|
104
|
+
};
|
|
105
|
+
} catch (err) {
|
|
106
|
+
request.log.error(
|
|
107
|
+
{
|
|
108
|
+
err,
|
|
109
|
+
issuedFor: tokenData.type === 'delegated' ? tokenData.label : `agent:${tokenData.label}`,
|
|
110
|
+
mode: tokenData.type === 'delegated' ? 'delegated' : 'hardware-bound',
|
|
111
|
+
},
|
|
112
|
+
'Enrollment CSR signing failed',
|
|
113
|
+
);
|
|
114
|
+
const statusCode = err.statusCode || 500;
|
|
115
|
+
// Only pass through client error messages (4xx); hide internal details for 5xx
|
|
116
|
+
const message = statusCode < 500 ? err.message : 'Enrollment failed';
|
|
117
|
+
return reply.code(statusCode).send({ error: message });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// Read version once at module load time — not on every request
|
|
9
|
+
let version = '0.0.0';
|
|
10
|
+
try {
|
|
11
|
+
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
12
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
|
13
|
+
version = pkg.version || '0.0.0';
|
|
14
|
+
} catch {
|
|
15
|
+
// If package.json can't be read, use fallback version
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default async function healthRoutes(fastify, _opts) {
|
|
19
|
+
fastify.get('/health', async (_request, _reply) => {
|
|
20
|
+
return { status: 'ok', version };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readInvitations, writeInvitations } from '../lib/state.js';
|
|
4
|
+
import {
|
|
5
|
+
createUserFromInvitation,
|
|
6
|
+
hashPassword,
|
|
7
|
+
readUsersRaw,
|
|
8
|
+
generateTotpSecret,
|
|
9
|
+
writeTotpToDatabase,
|
|
10
|
+
} from '../lib/authelia.js';
|
|
11
|
+
import { getConfig } from '../lib/config.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find an invitation by token using a constant-time comparison.
|
|
15
|
+
* Both the candidate and stored token are validated as 64-hex strings, so
|
|
16
|
+
* we can compare the raw byte buffers via timingSafeEqual without HMAC equalization.
|
|
17
|
+
* Iterating the full list (no early exit on match) keeps the lookup time
|
|
18
|
+
* proportional to inventory size, not which entry matched.
|
|
19
|
+
*/
|
|
20
|
+
function findInvitationByToken(invitations, token) {
|
|
21
|
+
const candidate = Buffer.from(token, 'hex');
|
|
22
|
+
if (candidate.length !== 32) return undefined;
|
|
23
|
+
let match;
|
|
24
|
+
for (const inv of invitations) {
|
|
25
|
+
if (typeof inv.token !== 'string' || !/^[a-f0-9]{64}$/.test(inv.token)) continue;
|
|
26
|
+
const stored = Buffer.from(inv.token, 'hex');
|
|
27
|
+
if (stored.length === candidate.length && crypto.timingSafeEqual(stored, candidate)) {
|
|
28
|
+
match = inv;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return match;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const AcceptInvitationSchema = z.object({
|
|
35
|
+
password: z.string().min(8).max(128),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const TokenParamSchema = z.object({ token: z.string().regex(/^[a-f0-9]{64}$/) });
|
|
39
|
+
|
|
40
|
+
export default async function inviteRoutes(fastify, _opts) {
|
|
41
|
+
// GET /api/invite/:token — get invitation details (public)
|
|
42
|
+
fastify.get('/:token', async (request, reply) => {
|
|
43
|
+
const { token } = TokenParamSchema.parse(request.params);
|
|
44
|
+
|
|
45
|
+
let invitations;
|
|
46
|
+
try {
|
|
47
|
+
invitations = await readInvitations();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
request.log.error(err, 'Failed to read invitations');
|
|
50
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const invitation = findInvitationByToken(invitations, token);
|
|
54
|
+
if (!invitation) {
|
|
55
|
+
return reply.code(404).send({ error: 'Invitation not found' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (invitation.used) {
|
|
59
|
+
return reply.code(410).send({ error: 'This invitation has already been used' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (new Date(invitation.expiresAt) < new Date()) {
|
|
63
|
+
return reply.code(410).send({ error: 'This invitation has expired' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
username: invitation.username,
|
|
68
|
+
email: invitation.email,
|
|
69
|
+
expiresAt: invitation.expiresAt,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// POST /api/invite/:token/accept — accept invitation and set password (public)
|
|
74
|
+
fastify.post('/:token/accept', async (request, reply) => {
|
|
75
|
+
const { token } = TokenParamSchema.parse(request.params);
|
|
76
|
+
const body = AcceptInvitationSchema.parse(request.body);
|
|
77
|
+
|
|
78
|
+
let invitations;
|
|
79
|
+
try {
|
|
80
|
+
invitations = await readInvitations();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
request.log.error(err, 'Failed to read invitations');
|
|
83
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const invitation = findInvitationByToken(invitations, token);
|
|
87
|
+
if (!invitation) {
|
|
88
|
+
return reply.code(404).send({ error: 'Invitation not found' });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (invitation.used) {
|
|
92
|
+
return reply.code(410).send({ error: 'This invitation has already been used' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (new Date(invitation.expiresAt) < new Date()) {
|
|
96
|
+
return reply.code(410).send({ error: 'This invitation has expired' });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Race condition guard: check user doesn't already exist
|
|
100
|
+
let usersData;
|
|
101
|
+
try {
|
|
102
|
+
usersData = await readUsersRaw();
|
|
103
|
+
} catch {
|
|
104
|
+
usersData = { users: {} };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (usersData.users[invitation.username]) {
|
|
108
|
+
return reply.code(409).send({ error: 'This username already exists' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Hash password and create user
|
|
112
|
+
let hashedPassword;
|
|
113
|
+
try {
|
|
114
|
+
hashedPassword = await hashPassword(body.password);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
request.log.error(err, 'Failed to hash password');
|
|
117
|
+
return reply.code(500).send({ error: 'Failed to process password' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await createUserFromInvitation(
|
|
122
|
+
invitation.username,
|
|
123
|
+
invitation.email,
|
|
124
|
+
invitation.groups,
|
|
125
|
+
hashedPassword,
|
|
126
|
+
);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
request.log.error(err, 'Failed to create user from invitation');
|
|
129
|
+
return reply.code(500).send({ error: 'Failed to create user account' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Generate TOTP so the user can scan the QR code immediately
|
|
133
|
+
let totpUri = null;
|
|
134
|
+
try {
|
|
135
|
+
const { secret, uri } = generateTotpSecret(invitation.username);
|
|
136
|
+
await writeTotpToDatabase(invitation.username, secret);
|
|
137
|
+
totpUri = uri;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
request.log.error(err, 'Failed to generate TOTP for invited user');
|
|
140
|
+
// Non-fatal: user was created, they can have admin reset TOTP later
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Mark invitation as used
|
|
144
|
+
invitation.used = true;
|
|
145
|
+
invitation.acceptedAt = new Date().toISOString();
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await writeInvitations(invitations);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
request.log.warn(err, 'Failed to update invitation status (user was created)');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const config = getConfig();
|
|
154
|
+
const loginUrl = config.domain ? `https://auth.${config.domain}/` : null;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
username: invitation.username,
|
|
159
|
+
loginUrl,
|
|
160
|
+
totpUri,
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent self-service routes.
|
|
3
|
+
*
|
|
4
|
+
* `/api/agents/me/*` returns information scoped to the agent identified by
|
|
5
|
+
* its mTLS client certificate (CN=agent:<label>). Admins must specify the
|
|
6
|
+
* label explicitly via separate routes — they have no implicit "me" agent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getChiselCredential, getChiselCredentialIssuedAt } from '../../lib/chisel-users.js';
|
|
10
|
+
import {
|
|
11
|
+
rotateAgentChiselCredential,
|
|
12
|
+
getAgentCapabilities,
|
|
13
|
+
getAgentAllowedSites,
|
|
14
|
+
} from '../../lib/mtls.js';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { AGENT_LABEL_REGEX } from '@lamalibre/lamaste';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* An agent may rotate its own chisel credential at most this often. Every
|
|
20
|
+
* rotation restarts chisel (the old password must stop working), which drops
|
|
21
|
+
* every agent's tunnels for a few seconds.
|
|
22
|
+
*/
|
|
23
|
+
const SELF_ROTATE_MIN_INTERVAL_MS = 10 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
const LabelParamSchema = z.object({
|
|
26
|
+
label: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(50)
|
|
30
|
+
.regex(/^[a-z0-9-]+$/, 'Invalid agent label'),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default async function agentsRoutes(fastify, _opts) {
|
|
34
|
+
// ------------------------------------------------------------------
|
|
35
|
+
// GET /agents/me/chisel-credential
|
|
36
|
+
//
|
|
37
|
+
// Returns the calling agent's chisel tunnel-server credential.
|
|
38
|
+
// Identifies the agent strictly via the mTLS cert label — never accepts
|
|
39
|
+
// a label query parameter, so a compromised agent cannot leak others'
|
|
40
|
+
// credentials.
|
|
41
|
+
// ------------------------------------------------------------------
|
|
42
|
+
fastify.get(
|
|
43
|
+
'/agents/me/chisel-credential',
|
|
44
|
+
{
|
|
45
|
+
preHandler: fastify.requireRole(['agent'], { capability: 'tunnels:read' }),
|
|
46
|
+
},
|
|
47
|
+
async (request, reply) => {
|
|
48
|
+
const label = request.certLabel;
|
|
49
|
+
if (!label) {
|
|
50
|
+
return reply.code(400).send({
|
|
51
|
+
error: 'Agent label could not be determined from client certificate',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const credential = await getChiselCredential(label);
|
|
55
|
+
if (!credential) {
|
|
56
|
+
return reply.code(404).send({
|
|
57
|
+
error: 'No chisel credential found for this agent',
|
|
58
|
+
hint:
|
|
59
|
+
'Ask an admin to run `lamaste-server chisel rotate-credential --label ' +
|
|
60
|
+
label +
|
|
61
|
+
'` to provision a credential.',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return credential;
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// ------------------------------------------------------------------
|
|
69
|
+
// POST /agents/me/chisel-credential/rotate
|
|
70
|
+
//
|
|
71
|
+
// Replaces the calling agent's chisel credential and returns the new one.
|
|
72
|
+
// An upgraded agent calls this once: versions before the credential moved
|
|
73
|
+
// into a 0600 environment file exposed it in process arguments and a
|
|
74
|
+
// world-readable unit file. Identity comes strictly from the mTLS cert.
|
|
75
|
+
// ------------------------------------------------------------------
|
|
76
|
+
fastify.post(
|
|
77
|
+
'/agents/me/chisel-credential/rotate',
|
|
78
|
+
{
|
|
79
|
+
preHandler: fastify.requireRole(['agent'], { capability: 'tunnels:read' }),
|
|
80
|
+
},
|
|
81
|
+
async (request, reply) => {
|
|
82
|
+
const label = request.certLabel;
|
|
83
|
+
if (!label) {
|
|
84
|
+
return reply.code(400).send({
|
|
85
|
+
error: 'Agent label could not be determined from client certificate',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!AGENT_LABEL_REGEX.test(label)) {
|
|
89
|
+
return reply
|
|
90
|
+
.code(400)
|
|
91
|
+
.send({ error: 'Plugin-agents do not use chisel tunnel credentials' });
|
|
92
|
+
}
|
|
93
|
+
const issuedAt = await getChiselCredentialIssuedAt(label);
|
|
94
|
+
if (issuedAt && Date.now() - Date.parse(issuedAt) < SELF_ROTATE_MIN_INTERVAL_MS) {
|
|
95
|
+
return reply.code(429).send({
|
|
96
|
+
error: 'The chisel credential was issued less than 10 minutes ago',
|
|
97
|
+
issuedAt,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const result = await rotateAgentChiselCredential(label, request.log);
|
|
102
|
+
return {
|
|
103
|
+
user: result.user,
|
|
104
|
+
password: result.password,
|
|
105
|
+
createdAt: result.createdAt,
|
|
106
|
+
};
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const status = err.statusCode || 500;
|
|
109
|
+
return reply.code(status).send({
|
|
110
|
+
error: err.message || 'Failed to rotate chisel credential',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// ------------------------------------------------------------------
|
|
117
|
+
// GET /agents/me/capabilities
|
|
118
|
+
//
|
|
119
|
+
// Returns the calling agent's capabilities and allowedSites. Used by the
|
|
120
|
+
// agent daemon (lamaste-agentd) to enforce per-route capability checks
|
|
121
|
+
// when a non-admin mTLS cert calls its REST API.
|
|
122
|
+
//
|
|
123
|
+
// Cert identity is taken strictly from `request.certLabel` (set by the
|
|
124
|
+
// mtls middleware from the cert CN). No body/query inputs — a compromised
|
|
125
|
+
// agent cannot ask for another agent's capabilities.
|
|
126
|
+
//
|
|
127
|
+
// Admin callers need an explicit label since they have no implicit "me"
|
|
128
|
+
// agent — they get an empty payload (admins are unconstrained).
|
|
129
|
+
// ------------------------------------------------------------------
|
|
130
|
+
fastify.get(
|
|
131
|
+
'/agents/me/capabilities',
|
|
132
|
+
{
|
|
133
|
+
preHandler: fastify.requireRole(['admin', 'agent']),
|
|
134
|
+
},
|
|
135
|
+
async (request, reply) => {
|
|
136
|
+
// Admin has no "me" agent — return empty (admin is master, no caps needed)
|
|
137
|
+
if (request.certRole === 'admin') {
|
|
138
|
+
return { capabilities: [], allowedSites: [], role: 'admin' };
|
|
139
|
+
}
|
|
140
|
+
const label = request.certLabel;
|
|
141
|
+
if (!label) {
|
|
142
|
+
return reply.code(400).send({
|
|
143
|
+
error: 'Agent label could not be determined from client certificate',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// Re-fetch from registry rather than reusing request.certCapabilities.
|
|
147
|
+
// The mtls middleware filters caps against currently-valid ones, but
|
|
148
|
+
// an explicit registry read here is the canonical source — keeps this
|
|
149
|
+
// endpoint robust to middleware changes.
|
|
150
|
+
const capabilities = await getAgentCapabilities(label);
|
|
151
|
+
const allowedSites = await getAgentAllowedSites(label);
|
|
152
|
+
return { capabilities, allowedSites, role: request.certRole };
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// ------------------------------------------------------------------
|
|
157
|
+
// POST /agents/:label/chisel-credential/rotate (admin only)
|
|
158
|
+
//
|
|
159
|
+
// Mints a new chisel password for the named agent and writes the
|
|
160
|
+
// updated authfile. Triggers a chisel restart. Returns the new
|
|
161
|
+
// credential so the admin can hand it off out-of-band — but the
|
|
162
|
+
// expected flow is for the agent to re-fetch via the /me endpoint.
|
|
163
|
+
// ------------------------------------------------------------------
|
|
164
|
+
fastify.post(
|
|
165
|
+
'/agents/:label/chisel-credential/rotate',
|
|
166
|
+
{
|
|
167
|
+
preHandler: fastify.requireRole(['admin']),
|
|
168
|
+
},
|
|
169
|
+
async (request, reply) => {
|
|
170
|
+
const params = LabelParamSchema.parse(request.params);
|
|
171
|
+
try {
|
|
172
|
+
const result = await rotateAgentChiselCredential(params.label, request.log);
|
|
173
|
+
return result;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const status = err.statusCode || 500;
|
|
176
|
+
return reply.code(status).send({
|
|
177
|
+
error: err.message || 'Failed to rotate chisel credential',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
}
|