feat: trusted release keys setting; drop the duplicate release registration route [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

The panel now lets the deployer decide which release keys it trusts. The
official CI key is built in and always trusted; an own P-256 public key can be
added (off by default, the private key is rejected) so manifests signed with it
are accepted too. Removing it drops the manifests only it signed. Changes are
audited and need update.confirm.

Admin UI: System > Trust > "Trusted release keys" with a collapsible guide on
generating a key pair and signing a manifest with openssl.

Registration now has one path, POST /api/release/offer (no token, signature
verified). The admin POST /releases route, the release.publish capability and
the ci-release preset are removed.
This commit is contained in:
justme committed 2026-10-08 18:50:16 +08:00
1 parent 904ea2b0ae
commit 0f8f3f9a54
24 files changed
+914 -325

No files matched your search

+212 -116
View File
@@ -379,24 +379,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/admin/releases": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** The latest signed release manifest of each component */
get: operations["releases_list"];
put?: never;
/** Register a CI-signed release manifest (the signature is checked against the release key); online nodes get it right away. Admin or release tokens */
post: operations["releases_publish"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/admin/auth/logout": {
parameters: {
query?: never;
@@ -989,6 +971,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/admin/releases": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** The latest signed release manifest of each component */
get: operations["releases_list"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/admin/nodes/{id}/update": {
parameters: {
query?: never;
@@ -1041,6 +1040,25 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/admin/release-key": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** The release keys this panel trusts: the built-in official key and the optional own key */
get: operations["release_key_get"];
/** Trust your own release public key (P-256 PEM; never the private key) in addition to the official one */
put: operations["release_key_set"];
post?: never;
/** Stop trusting your own release key; manifests only it signed are dropped */
delete: operations["release_key_clear"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/admin/links": {
parameters: {
query?: never;
@@ -2017,6 +2035,14 @@ export interface components {
/** @default release */
channel: string;
};
ReleaseKeyDto: {
fingerprint: string;
public_key: string;
};
ReleaseKeysDto: {
official: components["schemas"]["ReleaseKeyDto"];
custom: null | components["schemas"]["ReleaseKeyDto"];
};
ScoreDto: {
/** Format: double */
score: null | number | string;
@@ -2055,6 +2081,9 @@ export interface components {
SetLinkRequest: {
enabled: null | boolean;
};
SetReleaseKeyRequest: {
public_key: null | string;
};
SettingDto: {
key: string;
type: string;
@@ -3235,104 +3264,6 @@ export interface operations {
};
};
};
releases_list: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PageDtoOfReleaseDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
releases_publish: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["PublishReleaseRequest"];
};
};
responses: {
/** @description Created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseDto"];
};
};
/** @description Bad Request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Conflict */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
auth_logout: {
parameters: {
query?: never;
@@ -5658,6 +5589,44 @@ export interface operations {
};
};
};
releases_list: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PageDtoOfReleaseDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
nodes_update: {
parameters: {
query?: never;
@@ -5841,6 +5810,133 @@ export interface operations {
};
};
};
release_key_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseKeysDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
release_key_set: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SetReleaseKeyRequest"];
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseKeysDto"];
};
};
/** @description Bad Request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
release_key_clear: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseKeysDto"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
links_list: {
parameters: {
query?: never;
+2
View File
@@ -21,6 +21,8 @@ export type NodeBuildDto = Narrow<S['NodeBuildDto']>
export type NodeUpdateDto = Narrow<S['NodeUpdateDto']>
export type NodeOwnerDto = Narrow<S['NodeOwnerDto']>
export type ReleaseDto = Narrow<S['ReleaseDto']>
export type ReleaseKeysDto = Narrow<S['ReleaseKeysDto']>
export type ReleaseKeyDto = Narrow<S['ReleaseKeyDto']>
export type LinkDto = Narrow<S['LinkDto']>
export type LinkMatrixDto = Narrow<S['LinkMatrixDto']>
export type OverviewDto = Narrow<S['OverviewDto']>