@lamalibre/create-lamaste 4.2.0 → 4.3.1
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/package.json +2 -2
- package/scripts/bundle-vendor.js +13 -8
- package/scripts/lamaste-priv +205 -2
- package/src/index.js +8 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/panel-pin.js +41 -0
- package/src/lib/relay-services.js +15 -1
- package/src/lib/service-config.js +113 -0
- package/src/tasks/gatekeeper.js +8 -3
- package/src/tasks/node.js +34 -75
- package/src/tasks/redeploy.js +36 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -3
- package/vendor/docs/01-concepts/authorization.md +29 -17
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
- package/vendor/docs/01-concepts/security-model.md +110 -35
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +34 -16
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
- package/vendor/docs/02-guides/desktop-app-setup.md +11 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
- package/vendor/docs/03-architecture/overview.md +8 -8
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
- package/vendor/docs/04-api-reference/app-host.md +360 -0
- package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
- package/vendor/docs/04-api-reference/certificates.md +95 -38
- package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -111
- package/vendor/docs/04-api-reference/registry-and-oidc.md +8 -4
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +3 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +25 -20
- package/vendor/docs/06-reference/config-files.md +195 -97
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +33 -8
- package/vendor/docs/_index.json +20 -0
- package/vendor/docs/gatekeeper-design.md +22 -19
- package/vendor/docs/identity-directory-design.md +57 -56
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +47 -8
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +4 -3
- package/vendor/server/bin/lamaste-server.js +25 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/panel-pin.js +40 -0
- package/vendor/server/src/commands/plugins.js +963 -12
- package/vendor/server/src/commands/push-install.js +317 -0
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/uninstall.js +28 -1
- package/vendor/server/src/ndjson.js +4 -2
- package/vendor/server/src/panel-api.js +277 -34
- package/vendor/server/src/panel-identity.js +165 -0
- package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +84 -0
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +56 -4
- package/vendor/serverd/src/lib/actor.js +24 -0
- package/vendor/serverd/src/lib/agent-jobs.js +330 -0
- package/vendor/serverd/src/lib/app-error.js +10 -0
- package/vendor/serverd/src/lib/audit.js +145 -0
- package/vendor/serverd/src/lib/hook-effects.js +123 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
- package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
- package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
- package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
- package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +89 -8
- package/vendor/serverd/src/lib/nginx.js +298 -44
- package/vendor/serverd/src/lib/notifications.js +107 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
- package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
- package/vendor/serverd/src/lib/plugin-operations.js +295 -0
- package/vendor/serverd/src/lib/plugin-packages.js +249 -0
- package/vendor/serverd/src/lib/plugins.js +298 -68
- package/vendor/serverd/src/lib/push-install.js +88 -2
- package/vendor/serverd/src/lib/relay-hooks.js +100 -0
- package/vendor/serverd/src/lib/relay-signing.js +43 -0
- package/vendor/serverd/src/lib/request-hooks.js +79 -0
- package/vendor/serverd/src/lib/retained-data.js +99 -0
- package/vendor/serverd/src/lib/trust-proxy.js +34 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
- package/vendor/serverd/src/middleware/twofa-session.js +11 -1
- package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
- package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
- package/vendor/serverd/src/routes/management/agents.js +262 -1
- package/vendor/serverd/src/routes/management/audit.js +81 -0
- package/vendor/serverd/src/routes/management/certs.js +1 -15
- package/vendor/serverd/src/routes/management/plugins.js +569 -54
- package/vendor/serverd/src/routes/management/tunnels.js +26 -0
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
|
@@ -11,11 +11,24 @@ import {
|
|
|
11
11
|
rotateAgentChiselCredential,
|
|
12
12
|
getAgentCapabilities,
|
|
13
13
|
getAgentAllowedSites,
|
|
14
|
+
setAgentHostFacts,
|
|
14
15
|
} from '../../lib/mtls.js';
|
|
15
16
|
import { z } from 'zod';
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
AGENT_LABEL_REGEX,
|
|
19
|
+
AgentJobResultSchema,
|
|
20
|
+
HostFactsSchema,
|
|
21
|
+
pluginHostCapability,
|
|
22
|
+
} from '@lamalibre/lamaste';
|
|
17
23
|
import { RegistryCredentialError } from '@lamalibre/lamaste/server';
|
|
18
24
|
import { acquireAgentToken } from '../../lib/registry-credential.js';
|
|
25
|
+
import { agentPluginConfig, acknowledgeAgentPluginConfig } from '../../lib/plugin-hosting.js';
|
|
26
|
+
import {
|
|
27
|
+
AGENT_JOB_RESULT_MAX_BYTES,
|
|
28
|
+
completeAgentJob,
|
|
29
|
+
findAgentJob,
|
|
30
|
+
} from '../../lib/agent-jobs.js';
|
|
31
|
+
import { relayVerifyKeys } from '../../lib/relay-signing.js';
|
|
19
32
|
|
|
20
33
|
/**
|
|
21
34
|
* An agent may rotate its own chisel credential at most this often. Every
|
|
@@ -24,6 +37,20 @@ import { acquireAgentToken } from '../../lib/registry-credential.js';
|
|
|
24
37
|
*/
|
|
25
38
|
const SELF_ROTATE_MIN_INTERVAL_MS = 10 * 60 * 1000;
|
|
26
39
|
|
|
40
|
+
const PluginRouteParamSchema = z.object({
|
|
41
|
+
route: z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(100)
|
|
45
|
+
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, 'Invalid plugin route'),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const AckBodySchema = z.object({
|
|
49
|
+
issuedAt: z.string().min(1).max(64),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const JobIdParamSchema = z.object({ id: z.string().uuid() });
|
|
53
|
+
|
|
27
54
|
const LabelParamSchema = z.object({
|
|
28
55
|
label: z
|
|
29
56
|
.string()
|
|
@@ -32,6 +59,34 @@ const LabelParamSchema = z.object({
|
|
|
32
59
|
.regex(/^[a-z0-9-]+$/, 'Invalid agent label'),
|
|
33
60
|
});
|
|
34
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The plugin-config routes admit only a machine agent whose certificate
|
|
64
|
+
* carries the plugin's host capability. The admin master role passes every
|
|
65
|
+
* `requireRole` guard but has no agent identity to serve, so it is refused
|
|
66
|
+
* here too. Returns the reply when refused, undefined when admitted.
|
|
67
|
+
*/
|
|
68
|
+
function refuseUnlessHostingAgent(request, reply, route) {
|
|
69
|
+
if (request.certRole !== 'agent') {
|
|
70
|
+
return reply
|
|
71
|
+
.code(403)
|
|
72
|
+
.send({ error: 'Only the hosting agent certificate can read a plugin config' });
|
|
73
|
+
}
|
|
74
|
+
const label = request.certLabel;
|
|
75
|
+
if (!label || !AGENT_LABEL_REGEX.test(label)) {
|
|
76
|
+
return reply
|
|
77
|
+
.code(400)
|
|
78
|
+
.send({ error: 'Agent label could not be determined from client certificate' });
|
|
79
|
+
}
|
|
80
|
+
const required = pluginHostCapability(route);
|
|
81
|
+
if (!(request.certCapabilities || []).includes(required)) {
|
|
82
|
+
return reply.code(403).send({
|
|
83
|
+
error: 'Insufficient certificate capability',
|
|
84
|
+
details: { required, granted: request.certCapabilities || [] },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
35
90
|
export default async function agentsRoutes(fastify, _opts) {
|
|
36
91
|
// ------------------------------------------------------------------
|
|
37
92
|
// GET /agents/me/chisel-credential
|
|
@@ -194,6 +249,175 @@ export default async function agentsRoutes(fastify, _opts) {
|
|
|
194
249
|
},
|
|
195
250
|
);
|
|
196
251
|
|
|
252
|
+
// ------------------------------------------------------------------
|
|
253
|
+
// GET /agents/me/plugin-config/:route
|
|
254
|
+
//
|
|
255
|
+
// The config of an agent-hosted plugin hosted on the calling agent: the
|
|
256
|
+
// port its agent half listens on and the installation secret (plus the
|
|
257
|
+
// previous secret during a rotation). Only the hosting agent can read it:
|
|
258
|
+
// its certificate must carry `plugin:<route>:host`, which hosting granted,
|
|
259
|
+
// and the hosting must name it. Identity comes strictly from the mTLS
|
|
260
|
+
// certificate.
|
|
261
|
+
// ------------------------------------------------------------------
|
|
262
|
+
fastify.get(
|
|
263
|
+
'/agents/me/plugin-config/:route',
|
|
264
|
+
{
|
|
265
|
+
preHandler: fastify.requireRole(['agent']),
|
|
266
|
+
config: { rateLimit: { max: 60, timeWindow: '1 minute' } },
|
|
267
|
+
},
|
|
268
|
+
async (request, reply) => {
|
|
269
|
+
const { route } = PluginRouteParamSchema.parse(request.params);
|
|
270
|
+
const refused = refuseUnlessHostingAgent(request, reply, route);
|
|
271
|
+
if (refused) return refused;
|
|
272
|
+
const config = await agentPluginConfig(request.certLabel, route);
|
|
273
|
+
if (!config) {
|
|
274
|
+
return reply.code(404).send({ error: `Plugin "${route}" is not hosted on this agent` });
|
|
275
|
+
}
|
|
276
|
+
return config;
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// ------------------------------------------------------------------
|
|
281
|
+
// POST /agents/me/plugin-config/:route/ack
|
|
282
|
+
//
|
|
283
|
+
// The hosting agent confirms it holds the secret issued at `issuedAt`.
|
|
284
|
+
// From then on the panel's forwarder signs with it and drops the previous
|
|
285
|
+
// one. An `issuedAt` that is not the current secret's is answered with
|
|
286
|
+
// `acknowledged: false` and the current `secretIssuedAt`, so the agent
|
|
287
|
+
// fetches again.
|
|
288
|
+
// ------------------------------------------------------------------
|
|
289
|
+
fastify.post(
|
|
290
|
+
'/agents/me/plugin-config/:route/ack',
|
|
291
|
+
{
|
|
292
|
+
preHandler: fastify.requireRole(['agent']),
|
|
293
|
+
config: { rateLimit: { max: 60, timeWindow: '1 minute' } },
|
|
294
|
+
},
|
|
295
|
+
async (request, reply) => {
|
|
296
|
+
const { route } = PluginRouteParamSchema.parse(request.params);
|
|
297
|
+
const body = AckBodySchema.parse(request.body);
|
|
298
|
+
const refused = refuseUnlessHostingAgent(request, reply, route);
|
|
299
|
+
if (refused) return refused;
|
|
300
|
+
const result = await acknowledgeAgentPluginConfig(request.certLabel, route, body.issuedAt);
|
|
301
|
+
if (!result) {
|
|
302
|
+
return reply.code(404).send({ error: `Plugin "${route}" is not hosted on this agent` });
|
|
303
|
+
}
|
|
304
|
+
request.log.info(
|
|
305
|
+
{ label: request.certLabel, plugin: route, acknowledged: result.acknowledged },
|
|
306
|
+
'Plugin config acknowledgement',
|
|
307
|
+
);
|
|
308
|
+
return { ok: true, ...result };
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// ------------------------------------------------------------------
|
|
313
|
+
// POST /agents/me/jobs/:id/result
|
|
314
|
+
//
|
|
315
|
+
// The calling agent reports a job agent-config gave it. A hook job (a
|
|
316
|
+
// lifecycle hook call to its agent half): `{ ok: true, decision }`; an
|
|
317
|
+
// app job (install, activate or remove its agent half through the app
|
|
318
|
+
// host helper): `{ ok: true, app: { version, unit } }`; either:
|
|
319
|
+
// `{ ok: false, error }` (one line). The result must fit the job's kind
|
|
320
|
+
// (core `AgentJobResultSchema`). Only the machine agent the job is for,
|
|
321
|
+
// and only once. A hook job's agent must also hold the host capability of
|
|
322
|
+
// the job's plugin (it calls the hosted agent half); an app job's need
|
|
323
|
+
// not — purging the data an agent half left runs after the hosting, and
|
|
324
|
+
// the capability, are gone.
|
|
325
|
+
// ------------------------------------------------------------------
|
|
326
|
+
fastify.post(
|
|
327
|
+
'/agents/me/jobs/:id/result',
|
|
328
|
+
{
|
|
329
|
+
preHandler: fastify.requireRole(['agent']),
|
|
330
|
+
config: { rateLimit: { max: 60, timeWindow: '1 minute' } },
|
|
331
|
+
bodyLimit: AGENT_JOB_RESULT_MAX_BYTES,
|
|
332
|
+
},
|
|
333
|
+
async (request, reply) => {
|
|
334
|
+
const { id } = JobIdParamSchema.parse(request.params);
|
|
335
|
+
const result = AgentJobResultSchema.parse(request.body);
|
|
336
|
+
const job = await findAgentJob(id);
|
|
337
|
+
if (!job || job.agentLabel !== request.certLabel) {
|
|
338
|
+
return reply.code(404).send({ error: 'Job not found' });
|
|
339
|
+
}
|
|
340
|
+
if (job.kind === 'hook') {
|
|
341
|
+
const refused = refuseUnlessHostingAgent(request, reply, job.route);
|
|
342
|
+
if (refused) return refused;
|
|
343
|
+
} else if (request.certRole !== 'agent') {
|
|
344
|
+
return reply
|
|
345
|
+
.code(403)
|
|
346
|
+
.send({ error: 'Only the agent certificate the job is for can report it' });
|
|
347
|
+
}
|
|
348
|
+
const outcome = await completeAgentJob(request.certLabel, id, result);
|
|
349
|
+
if (outcome === 'not_found') return reply.code(404).send({ error: 'Job not found' });
|
|
350
|
+
if (outcome === 'not_pending') {
|
|
351
|
+
return reply.code(409).send({ error: 'The job is no longer pending' });
|
|
352
|
+
}
|
|
353
|
+
if (outcome === 'invalid') {
|
|
354
|
+
return reply.code(400).send({
|
|
355
|
+
error:
|
|
356
|
+
job.kind === 'hook'
|
|
357
|
+
? 'A hook job reports { ok: true, decision } or { ok: false, error }'
|
|
358
|
+
: 'An app job reports { ok: true, app: { version, unit } } or { ok: false, error }',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
request.log.info(
|
|
362
|
+
{
|
|
363
|
+
label: request.certLabel,
|
|
364
|
+
plugin: job.route,
|
|
365
|
+
kind: job.kind,
|
|
366
|
+
...(job.stage ? { stage: job.stage } : {}),
|
|
367
|
+
ok: result.ok,
|
|
368
|
+
},
|
|
369
|
+
'Agent job reported',
|
|
370
|
+
);
|
|
371
|
+
return { ok: true };
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// ------------------------------------------------------------------
|
|
376
|
+
// PUT /agents/me/host-facts
|
|
377
|
+
//
|
|
378
|
+
// The calling agent reports what the relay needs to know about its host
|
|
379
|
+
// to tell which plugins it can host (`agent.requires`, and whether
|
|
380
|
+
// Lamaste can install an agent half there): OS, distribution,
|
|
381
|
+
// architecture, Node.js, systemd and its app host helper — core
|
|
382
|
+
// `HostFactsSchema`. Its sync sends them when they change. Kept on the
|
|
383
|
+
// agent's registry record (`hostFacts`, `hostFactsAt`); identity comes
|
|
384
|
+
// strictly from the mTLS certificate.
|
|
385
|
+
// ------------------------------------------------------------------
|
|
386
|
+
fastify.put(
|
|
387
|
+
'/agents/me/host-facts',
|
|
388
|
+
{
|
|
389
|
+
preHandler: fastify.requireRole(['agent']),
|
|
390
|
+
// Sent on change (and once a day); this only stops a loop.
|
|
391
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
392
|
+
bodyLimit: 4 * 1024,
|
|
393
|
+
},
|
|
394
|
+
async (request, reply) => {
|
|
395
|
+
// requireRole lets the admin master role through every guard; an admin
|
|
396
|
+
// certificate has no host to report. A plugin-agent hosts nothing.
|
|
397
|
+
if (request.certRole !== 'agent') {
|
|
398
|
+
return reply.code(403).send({ error: 'Only an agent certificate can report host facts' });
|
|
399
|
+
}
|
|
400
|
+
const label = request.certLabel;
|
|
401
|
+
if (!label || !AGENT_LABEL_REGEX.test(label)) {
|
|
402
|
+
return reply
|
|
403
|
+
.code(400)
|
|
404
|
+
.send({ error: 'Agent label could not be determined from client certificate' });
|
|
405
|
+
}
|
|
406
|
+
const facts = HostFactsSchema.parse(request.body);
|
|
407
|
+
try {
|
|
408
|
+
await setAgentHostFacts(label, facts);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const status = err.statusCode || 500;
|
|
411
|
+
return reply.code(status).send({ error: err.message || 'Failed to store host facts' });
|
|
412
|
+
}
|
|
413
|
+
request.log.info(
|
|
414
|
+
{ label, os: facts.os, arch: facts.arch, apphost: facts.apphost?.version ?? null },
|
|
415
|
+
'Agent reported host facts',
|
|
416
|
+
);
|
|
417
|
+
return { ok: true };
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
|
|
197
421
|
// ------------------------------------------------------------------
|
|
198
422
|
// GET /agents/me/capabilities
|
|
199
423
|
//
|
|
@@ -234,6 +458,43 @@ export default async function agentsRoutes(fastify, _opts) {
|
|
|
234
458
|
},
|
|
235
459
|
);
|
|
236
460
|
|
|
461
|
+
// ------------------------------------------------------------------
|
|
462
|
+
// GET /agents/me/relay-keys
|
|
463
|
+
//
|
|
464
|
+
// The public keys relay assertions are signed with
|
|
465
|
+
// (`X-Lamalibre-Relay-Assertion`, see relay-assertion.ts), and the label
|
|
466
|
+
// this relay knows the calling agent by — the audience of the assertions
|
|
467
|
+
// made for it, which the agent's local registry name need not equal.
|
|
468
|
+
// lamaste-agentd fetches both over its pinned mTLS channel and accepts a
|
|
469
|
+
// request from the relay only with an assertion one of the keys verifies,
|
|
470
|
+
// made for that label. `sudo lamaste-agent apphost enable|pin` pins the
|
|
471
|
+
// first key, and the label, for the agent's root helper, which runs only
|
|
472
|
+
// the app jobs this relay signed with it (app-job-signature.ts). The keys
|
|
473
|
+
// are public material: any agent or admin certificate may read them
|
|
474
|
+
// (`agentLabel` is null for non-agent callers).
|
|
475
|
+
// ------------------------------------------------------------------
|
|
476
|
+
fastify.get(
|
|
477
|
+
'/agents/me/relay-keys',
|
|
478
|
+
{
|
|
479
|
+
preHandler: fastify.requireRole(['admin', 'agent']),
|
|
480
|
+
config: { rateLimit: { max: 60, timeWindow: '1 minute' } },
|
|
481
|
+
},
|
|
482
|
+
async (request, reply) => {
|
|
483
|
+
try {
|
|
484
|
+
return {
|
|
485
|
+
keys: await relayVerifyKeys(),
|
|
486
|
+
agentLabel: request.certRole === 'agent' ? request.certLabel : null,
|
|
487
|
+
};
|
|
488
|
+
} catch (err) {
|
|
489
|
+
request.log.error(
|
|
490
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
491
|
+
'Relay signing key unusable',
|
|
492
|
+
);
|
|
493
|
+
return reply.code(503).send({ error: 'Relay signing key unavailable' });
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
);
|
|
497
|
+
|
|
237
498
|
// ------------------------------------------------------------------
|
|
238
499
|
// POST /agents/:label/chisel-credential/rotate (admin only)
|
|
239
500
|
//
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listAudit } from '../../lib/audit.js';
|
|
3
|
+
import { listNotifications, markNotificationsRead } from '../../lib/notifications.js';
|
|
4
|
+
|
|
5
|
+
const AuditQuerySchema = z.object({
|
|
6
|
+
plugin: z
|
|
7
|
+
.string()
|
|
8
|
+
.regex(/^[a-z0-9-]{1,100}$/)
|
|
9
|
+
.optional(),
|
|
10
|
+
action: z
|
|
11
|
+
.string()
|
|
12
|
+
.regex(/^[a-z][a-z0-9.-]{0,99}$/)
|
|
13
|
+
.optional(),
|
|
14
|
+
before: z.coerce.number().int().positive().optional(),
|
|
15
|
+
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const NotificationsQuerySchema = z.object({
|
|
19
|
+
unread: z.enum(['true', 'false']).optional(),
|
|
20
|
+
before: z.coerce.number().int().positive().optional(),
|
|
21
|
+
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const MarkReadBodySchema = z.union([
|
|
25
|
+
z.object({ all: z.literal(true) }).strict(),
|
|
26
|
+
z.object({ ids: z.array(z.number().int().positive()).min(1).max(200) }).strict(),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export default async function auditRoutes(fastify, _opts) {
|
|
30
|
+
// ------------------------------------------------------------------
|
|
31
|
+
// GET /audit — the audit log, newest first (?plugin, ?action — exact or
|
|
32
|
+
// a prefix ending in ".", ?before=<id>, ?limit)
|
|
33
|
+
// ------------------------------------------------------------------
|
|
34
|
+
fastify.get(
|
|
35
|
+
'/audit',
|
|
36
|
+
{
|
|
37
|
+
preHandler: fastify.requireRole(['admin']),
|
|
38
|
+
},
|
|
39
|
+
async (request) => {
|
|
40
|
+
const query = AuditQuerySchema.parse(request.query);
|
|
41
|
+
const entries = await listAudit(query);
|
|
42
|
+
return { entries };
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// ------------------------------------------------------------------
|
|
47
|
+
// GET /notifications — panel notifications, newest first, and the
|
|
48
|
+
// unread count
|
|
49
|
+
// ------------------------------------------------------------------
|
|
50
|
+
fastify.get(
|
|
51
|
+
'/notifications',
|
|
52
|
+
{
|
|
53
|
+
preHandler: fastify.requireRole(['admin']),
|
|
54
|
+
},
|
|
55
|
+
async (request) => {
|
|
56
|
+
const query = NotificationsQuerySchema.parse(request.query);
|
|
57
|
+
return listNotifications({
|
|
58
|
+
unreadOnly: query.unread === 'true',
|
|
59
|
+
before: query.before,
|
|
60
|
+
limit: query.limit,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ------------------------------------------------------------------
|
|
66
|
+
// POST /notifications/read — { ids: [...] } or { all: true }
|
|
67
|
+
// ------------------------------------------------------------------
|
|
68
|
+
fastify.post(
|
|
69
|
+
'/notifications/read',
|
|
70
|
+
{
|
|
71
|
+
preHandler: fastify.requireRole(['admin']),
|
|
72
|
+
},
|
|
73
|
+
async (request) => {
|
|
74
|
+
const body = MarkReadBodySchema.parse(request.body);
|
|
75
|
+
const changed = await markNotificationsRead(
|
|
76
|
+
'all' in body ? { all: true } : { ids: body.ids },
|
|
77
|
+
);
|
|
78
|
+
return { ok: true, changed };
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -25,6 +25,7 @@ import { getConfig, updateConfig } from '../../lib/config.js';
|
|
|
25
25
|
import { addToRevocationList } from '../../lib/revocation.js';
|
|
26
26
|
import { agentOwnsInstanceForScope } from '../../lib/tickets.js';
|
|
27
27
|
import * as nginx from '../../lib/nginx.js';
|
|
28
|
+
import { actorFromRequest } from '../../lib/actor.js';
|
|
28
29
|
|
|
29
30
|
const DomainParamSchema = z.object({
|
|
30
31
|
domain: z
|
|
@@ -142,21 +143,6 @@ const DelegatedEnrollBodySchema = z.object({
|
|
|
142
143
|
),
|
|
143
144
|
});
|
|
144
145
|
|
|
145
|
-
/**
|
|
146
|
-
* Derive a stable audit identifier for the calling principal.
|
|
147
|
-
*
|
|
148
|
-
* Returns the certificate CN form (`admin`, `agent:<label>`, `plugin-agent:...`)
|
|
149
|
-
* so a journalctl reader can correlate cert lifecycle events with the
|
|
150
|
-
* principal that initiated them. Returns `'unknown'` when middleware did not
|
|
151
|
-
* populate cert metadata (e.g. dev mode shortcut on /api/health).
|
|
152
|
-
*/
|
|
153
|
-
function actorFromRequest(request) {
|
|
154
|
-
if (request.certRole === 'admin') return 'admin';
|
|
155
|
-
if (request.certRole === 'agent' && request.certLabel) return `agent:${request.certLabel}`;
|
|
156
|
-
if (request.certRole === 'plugin-agent' && request.certLabel) return request.certLabel;
|
|
157
|
-
return 'unknown';
|
|
158
|
-
}
|
|
159
|
-
|
|
160
146
|
/**
|
|
161
147
|
* Parse certbot listing results into the unified cert response format.
|
|
162
148
|
*/
|