pepr 0.1.20 → 0.1.22
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/dist/pepr-cli.js +193 -54
- package/package.json +6 -2
- package/src/lib/k8s/tls.ts +71 -38
- package/src/lib/k8s/webhook.ts +98 -15
package/dist/pepr-cli.js
CHANGED
|
@@ -21,7 +21,7 @@ var uuid = require('uuid');
|
|
|
21
21
|
var commander = require('commander');
|
|
22
22
|
var chokidar = require('chokidar');
|
|
23
23
|
|
|
24
|
-
var version = "0.1.
|
|
24
|
+
var version = "0.1.22";
|
|
25
25
|
|
|
26
26
|
// SPDX-License-Identifier: Apache-2.0
|
|
27
27
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
@@ -91,47 +91,71 @@ const banner = `[107;40m[38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;
|
|
|
91
91
|
[38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m [38;5;016m
|
|
92
92
|
[0m`;
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
const caName = "Pepr Ephemeral CA";
|
|
95
|
+
/**
|
|
96
|
+
* Generates a self-signed CA and server certificate with Subject Alternative Names (SANs) for the K8s webhook.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} name - The name to use for the server certificate's Common Name and SAN DNS entry.
|
|
99
|
+
* @returns {TLSOut} - An object containing the Base64-encoded CA, server certificate, and server private key.
|
|
100
|
+
*/
|
|
95
101
|
function genTLS(name) {
|
|
96
|
-
// Generate a new CA key pair
|
|
102
|
+
// Generate a new CA key pair and create a self-signed CA certificate
|
|
97
103
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
98
|
-
const caCert =
|
|
99
|
-
caCert.
|
|
100
|
-
caCert.serialNumber = "01";
|
|
101
|
-
caCert.validity.notBefore = new Date();
|
|
102
|
-
caCert.validity.notAfter = new Date();
|
|
103
|
-
caCert.validity.notAfter.setFullYear(caCert.validity.notBefore.getFullYear() + 1);
|
|
104
|
-
const caAttrs = [
|
|
104
|
+
const caCert = genCert(caKeys, caName, [{ name: "commonName", value: caName }]);
|
|
105
|
+
caCert.setExtensions([
|
|
105
106
|
{
|
|
106
|
-
name: "
|
|
107
|
-
|
|
107
|
+
name: "basicConstraints",
|
|
108
|
+
cA: true,
|
|
108
109
|
},
|
|
109
|
-
];
|
|
110
|
-
caCert.setSubject(caAttrs);
|
|
111
|
-
caCert.setIssuer(caAttrs);
|
|
112
|
-
caCert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
113
|
-
// Generate a new key pair
|
|
114
|
-
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
115
|
-
const cert = forge.pki.createCertificate();
|
|
116
|
-
cert.publicKey = keys.publicKey;
|
|
117
|
-
cert.serialNumber = "01";
|
|
118
|
-
cert.validity.notBefore = new Date();
|
|
119
|
-
cert.validity.notAfter = new Date();
|
|
120
|
-
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
|
121
|
-
const attrs = [
|
|
122
110
|
{
|
|
123
|
-
name: "
|
|
124
|
-
|
|
111
|
+
name: "keyUsage",
|
|
112
|
+
keyCertSign: true,
|
|
113
|
+
digitalSignature: true,
|
|
114
|
+
nonRepudiation: true,
|
|
115
|
+
keyEncipherment: true,
|
|
116
|
+
dataEncipherment: true,
|
|
125
117
|
},
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
]);
|
|
119
|
+
// Generate a new server key pair and create a server certificate signed by the CA
|
|
120
|
+
const serverKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
121
|
+
const serverCert = genCert(serverKeys, name, caCert.subject.attributes);
|
|
122
|
+
// Sign both certificates with the CA private key
|
|
123
|
+
caCert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
124
|
+
serverCert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
130
125
|
// Convert the keys and certificates to PEM format
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
const pem = {
|
|
127
|
+
ca: forge.pki.certificateToPem(caCert),
|
|
128
|
+
crt: forge.pki.certificateToPem(serverCert),
|
|
129
|
+
key: forge.pki.privateKeyToPem(serverKeys.privateKey),
|
|
130
|
+
};
|
|
131
|
+
// Base64-encode the PEM strings
|
|
132
|
+
const ca = Buffer.from(pem.ca).toString("base64");
|
|
133
|
+
const key = Buffer.from(pem.key).toString("base64");
|
|
134
|
+
const crt = Buffer.from(pem.crt).toString("base64");
|
|
135
|
+
return { ca, key, crt, pem };
|
|
136
|
+
}
|
|
137
|
+
function genCert(key, name, issuer) {
|
|
138
|
+
const crt = forge.pki.createCertificate();
|
|
139
|
+
crt.publicKey = key.publicKey;
|
|
140
|
+
crt.serialNumber = "01";
|
|
141
|
+
crt.validity.notBefore = new Date();
|
|
142
|
+
crt.validity.notAfter = new Date();
|
|
143
|
+
crt.validity.notAfter.setFullYear(crt.validity.notBefore.getFullYear() + 1);
|
|
144
|
+
// Add SANs to the server certificate
|
|
145
|
+
crt.setExtensions([
|
|
146
|
+
{
|
|
147
|
+
name: "subjectAltName",
|
|
148
|
+
altNames: [
|
|
149
|
+
{
|
|
150
|
+
type: 2,
|
|
151
|
+
value: name,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
// Set the server certificate's issuer to the CA
|
|
157
|
+
crt.setIssuer(issuer);
|
|
158
|
+
return crt;
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
// SPDX-License-Identifier: Apache-2.0
|
|
@@ -141,12 +165,16 @@ const peprIgnore = {
|
|
|
141
165
|
values: ["ignore"],
|
|
142
166
|
};
|
|
143
167
|
class Webhook {
|
|
144
|
-
|
|
168
|
+
get tls() {
|
|
169
|
+
return this._tls;
|
|
170
|
+
}
|
|
171
|
+
constructor(config, host) {
|
|
145
172
|
this.config = config;
|
|
173
|
+
this.host = host;
|
|
146
174
|
this.name = `pepr-${config.uuid}`;
|
|
147
175
|
this.image = `ghcr.io/defenseunicorns/pepr/controller:${config.version}`;
|
|
148
176
|
// Generate the ephemeral tls things
|
|
149
|
-
this.
|
|
177
|
+
this._tls = genTLS(this.host || `${this.name}.pepr-system.svc`);
|
|
150
178
|
}
|
|
151
179
|
/** Generate the pepr-system namespace */
|
|
152
180
|
namespace() {
|
|
@@ -217,8 +245,8 @@ class Webhook {
|
|
|
217
245
|
},
|
|
218
246
|
type: "kubernetes.io/tls",
|
|
219
247
|
data: {
|
|
220
|
-
"tls.crt": this.
|
|
221
|
-
"tls.key": this.
|
|
248
|
+
"tls.crt": this._tls.crt,
|
|
249
|
+
"tls.key": this._tls.key,
|
|
222
250
|
},
|
|
223
251
|
};
|
|
224
252
|
}
|
|
@@ -233,6 +261,21 @@ class Webhook {
|
|
|
233
261
|
values: this.config.alwaysIgnore.namespaces,
|
|
234
262
|
});
|
|
235
263
|
}
|
|
264
|
+
const clientConfig = {
|
|
265
|
+
caBundle: this._tls.ca,
|
|
266
|
+
};
|
|
267
|
+
// If a host is specified, use that with a port of 3000
|
|
268
|
+
if (this.host) {
|
|
269
|
+
clientConfig.url = `https://${this.host}:3000/mutate`;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// Otherwise, use the service
|
|
273
|
+
clientConfig.service = {
|
|
274
|
+
name: this.name,
|
|
275
|
+
namespace: "pepr-system",
|
|
276
|
+
path: "/mutate",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
236
279
|
return {
|
|
237
280
|
apiVersion: "admissionregistration.k8s.io/v1",
|
|
238
281
|
kind: "MutatingWebhookConfiguration",
|
|
@@ -241,14 +284,7 @@ class Webhook {
|
|
|
241
284
|
{
|
|
242
285
|
name: `${name}.pepr.dev`,
|
|
243
286
|
admissionReviewVersions: ["v1", "v1beta1"],
|
|
244
|
-
clientConfig
|
|
245
|
-
caBundle: this.tls.ca,
|
|
246
|
-
service: {
|
|
247
|
-
name: this.name,
|
|
248
|
-
namespace: "pepr-system",
|
|
249
|
-
path: "/mutate",
|
|
250
|
-
},
|
|
251
|
-
},
|
|
287
|
+
clientConfig,
|
|
252
288
|
failurePolicy: "Ignore",
|
|
253
289
|
matchPolicy: "Equivalent",
|
|
254
290
|
timeoutSeconds: 15,
|
|
@@ -264,7 +300,7 @@ class Webhook {
|
|
|
264
300
|
apiGroups: ["*"],
|
|
265
301
|
apiVersions: ["*"],
|
|
266
302
|
operations: ["CREATE", "UPDATE", "DELETE"],
|
|
267
|
-
resources: ["
|
|
303
|
+
resources: ["*/*"],
|
|
268
304
|
},
|
|
269
305
|
],
|
|
270
306
|
// @todo: track side effects state
|
|
@@ -285,7 +321,7 @@ class Webhook {
|
|
|
285
321
|
},
|
|
286
322
|
},
|
|
287
323
|
spec: {
|
|
288
|
-
replicas:
|
|
324
|
+
replicas: 2,
|
|
289
325
|
selector: {
|
|
290
326
|
matchLabels: {
|
|
291
327
|
app: this.name,
|
|
@@ -333,6 +369,11 @@ class Webhook {
|
|
|
333
369
|
mountPath: "/etc/certs",
|
|
334
370
|
readOnly: true,
|
|
335
371
|
},
|
|
372
|
+
{
|
|
373
|
+
name: "module",
|
|
374
|
+
mountPath: "/app/module.js.gz",
|
|
375
|
+
readOnly: true,
|
|
376
|
+
},
|
|
336
377
|
],
|
|
337
378
|
},
|
|
338
379
|
],
|
|
@@ -343,12 +384,56 @@ class Webhook {
|
|
|
343
384
|
secretName: `${this.name}-tls`,
|
|
344
385
|
},
|
|
345
386
|
},
|
|
387
|
+
{
|
|
388
|
+
name: "module",
|
|
389
|
+
secret: {
|
|
390
|
+
secretName: `${this.name}-module`,
|
|
391
|
+
},
|
|
392
|
+
},
|
|
346
393
|
],
|
|
347
394
|
},
|
|
348
395
|
},
|
|
349
396
|
},
|
|
350
397
|
};
|
|
351
398
|
}
|
|
399
|
+
/** Only permit the */
|
|
400
|
+
networkPolicy() {
|
|
401
|
+
return {
|
|
402
|
+
apiVersion: "networking.k8s.io/v1",
|
|
403
|
+
kind: "NetworkPolicy",
|
|
404
|
+
metadata: {
|
|
405
|
+
name: this.name,
|
|
406
|
+
namespace: "pepr-system",
|
|
407
|
+
},
|
|
408
|
+
spec: {
|
|
409
|
+
podSelector: {
|
|
410
|
+
matchLabels: {
|
|
411
|
+
app: this.name,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
policyTypes: ["Ingress"],
|
|
415
|
+
ingress: [
|
|
416
|
+
{
|
|
417
|
+
from: [
|
|
418
|
+
{
|
|
419
|
+
namespaceSelector: {
|
|
420
|
+
matchLabels: {
|
|
421
|
+
"kubernetes.io/metadata.name": "kube-system",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
ports: [
|
|
427
|
+
{
|
|
428
|
+
protocol: "TCP",
|
|
429
|
+
port: 443,
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
352
437
|
service() {
|
|
353
438
|
return {
|
|
354
439
|
apiVersion: "v1",
|
|
@@ -415,6 +500,7 @@ class Webhook {
|
|
|
415
500
|
allYaml(code) {
|
|
416
501
|
const resources = [
|
|
417
502
|
this.namespace(),
|
|
503
|
+
this.networkPolicy(),
|
|
418
504
|
this.clusterRole(),
|
|
419
505
|
this.clusterRoleBinding(),
|
|
420
506
|
this.serviceAccount(),
|
|
@@ -437,6 +523,7 @@ class Webhook {
|
|
|
437
523
|
const rbacApi = kubeConfig.makeApiClient(clientNode.RbacAuthorizationV1Api);
|
|
438
524
|
const appsApi = kubeConfig.makeApiClient(clientNode.AppsV1Api);
|
|
439
525
|
const admissionApi = kubeConfig.makeApiClient(clientNode.AdmissionregistrationV1Api);
|
|
526
|
+
const networkApi = kubeConfig.makeApiClient(clientNode.NetworkingV1Api);
|
|
440
527
|
const ns = this.namespace();
|
|
441
528
|
try {
|
|
442
529
|
types.Log.info("Checking for namespace");
|
|
@@ -447,6 +534,16 @@ class Webhook {
|
|
|
447
534
|
types.Log.info("Creating namespace");
|
|
448
535
|
await coreV1Api.createNamespace(ns);
|
|
449
536
|
}
|
|
537
|
+
const netpol = this.networkPolicy();
|
|
538
|
+
try {
|
|
539
|
+
types.Log.info("Checking for network policy");
|
|
540
|
+
await networkApi.readNamespacedNetworkPolicy(netpol.metadata.name, namespace);
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
types.Log.debug(e.body);
|
|
544
|
+
types.Log.info("Creating network policy");
|
|
545
|
+
await networkApi.createNamespacedNetworkPolicy(namespace, netpol);
|
|
546
|
+
}
|
|
450
547
|
const wh = this.mutatingWebhook();
|
|
451
548
|
try {
|
|
452
549
|
types.Log.info("Creating mutating webhook");
|
|
@@ -496,6 +593,10 @@ class Webhook {
|
|
|
496
593
|
await coreV1Api.deleteNamespacedServiceAccount(sa.metadata.name, namespace);
|
|
497
594
|
await coreV1Api.createNamespacedServiceAccount(namespace, sa);
|
|
498
595
|
}
|
|
596
|
+
// If a host is specified, we don't need to deploy the rest of the resources
|
|
597
|
+
if (this.host) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
499
600
|
const mod = this.moduleSecret(code);
|
|
500
601
|
try {
|
|
501
602
|
types.Log.info("Creating module secret");
|
|
@@ -689,6 +790,47 @@ function deploy (program) {
|
|
|
689
790
|
});
|
|
690
791
|
}
|
|
691
792
|
|
|
793
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
794
|
+
function dev (program) {
|
|
795
|
+
program
|
|
796
|
+
.command("dev")
|
|
797
|
+
.description("Setup a local webhook development environment")
|
|
798
|
+
.option("-d, --dir [directory]", "Pepr module directory", ".")
|
|
799
|
+
.option("-h, --host [host]", "Host to listen on", "host.docker.internal")
|
|
800
|
+
.action(async (opts) => {
|
|
801
|
+
// Prompt the user to confirm
|
|
802
|
+
const confirm = await prompt.prompt({
|
|
803
|
+
type: "confirm",
|
|
804
|
+
name: "confirm",
|
|
805
|
+
message: "This will remove and redeploy the module. Continue?",
|
|
806
|
+
});
|
|
807
|
+
// Exit if the user doesn't confirm
|
|
808
|
+
if (!confirm.confirm) {
|
|
809
|
+
process.exit(0);
|
|
810
|
+
}
|
|
811
|
+
// Build the module
|
|
812
|
+
const { cfg, path } = await buildModule(opts.dir);
|
|
813
|
+
// Read the compiled module code
|
|
814
|
+
const code = await fs.promises.readFile(path, { encoding: "utf-8" });
|
|
815
|
+
// Generate a secret for the module
|
|
816
|
+
const webhook = new Webhook({
|
|
817
|
+
...cfg.pepr,
|
|
818
|
+
description: cfg.description,
|
|
819
|
+
}, opts.host);
|
|
820
|
+
// Write the TLS cert and key to disk
|
|
821
|
+
await fs.promises.writeFile("insecure-tls.crt", webhook.tls.pem.crt);
|
|
822
|
+
await fs.promises.writeFile("insecure-tls.key", webhook.tls.pem.key);
|
|
823
|
+
try {
|
|
824
|
+
await webhook.deploy(code);
|
|
825
|
+
types.Log.info(`Module deployed successfully`);
|
|
826
|
+
}
|
|
827
|
+
catch (e) {
|
|
828
|
+
types.Log.error(`Error deploying module: ${e}`);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
692
834
|
// SPDX-License-Identifier: Apache-2.0
|
|
693
835
|
/**
|
|
694
836
|
* Sanitize a user input name to be used as a pepr module directory name
|
|
@@ -1009,11 +1151,7 @@ function walkthrough() {
|
|
|
1009
1151
|
},
|
|
1010
1152
|
],
|
|
1011
1153
|
};
|
|
1012
|
-
return prompt([
|
|
1013
|
-
askName,
|
|
1014
|
-
askDescription,
|
|
1015
|
-
askErrorBehavior,
|
|
1016
|
-
]);
|
|
1154
|
+
return prompt([askName, askDescription, askErrorBehavior]);
|
|
1017
1155
|
}
|
|
1018
1156
|
async function confirm(dirName, packageJSON, peprTSPath) {
|
|
1019
1157
|
console.log(`
|
|
@@ -1147,4 +1285,5 @@ build(program);
|
|
|
1147
1285
|
capability(program);
|
|
1148
1286
|
test(program);
|
|
1149
1287
|
deploy(program);
|
|
1288
|
+
dev(program);
|
|
1150
1289
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pepr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Kubernetes application engine",
|
|
5
5
|
"author": "Defense Unicorns",
|
|
6
6
|
"homepage": "https://github.com/defenseunicorns/pepr",
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
"prettier": "npx prettier src --check",
|
|
30
30
|
"prettier:fix": "npm run prettier -- --write",
|
|
31
31
|
"prepublishOnly": "rm -fr dist/* && npm run lint:fix && npm run prettier:fix && npm run test && npm run build",
|
|
32
|
-
"e2e-dev": "
|
|
32
|
+
"e2e-dev-setup": "k3d cluster delete pepr-dev && k3d cluster create pepr-dev",
|
|
33
|
+
"e2e-dev": "npm run build && docker buildx build --tag pepr:dev . && k3d image import pepr:dev -c pepr-dev && node dist/pepr-cli.js deploy -f -i pepr:dev",
|
|
34
|
+
"prestart": "ts-node src/cli dev",
|
|
35
|
+
"start": "chokidar 'src/**/*.ts' -c 'SSL_KEY_PATH=insecure-tls.key SSL_CERT_PATH=insecure-tls.crt ts-node src/controller/index.ts' --initial --silent"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
38
|
"@kubernetes/client-node": "^0.18.1",
|
|
@@ -59,6 +62,7 @@
|
|
|
59
62
|
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
|
60
63
|
"@typescript-eslint/parser": "^5.57.0",
|
|
61
64
|
"ava": "^5.2.0",
|
|
65
|
+
"chokidar-cli": "^3.0.0",
|
|
62
66
|
"eslint": "^8.37.0",
|
|
63
67
|
"ts-node": "^10.9.1",
|
|
64
68
|
"tsconfig-paths": "^4.1.2"
|
package/src/lib/k8s/tls.ts
CHANGED
|
@@ -1,57 +1,90 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
-
|
|
4
|
-
// @todo: quick and dirty temp tls chain for testing, to be replaced at runtime
|
|
5
|
-
// Don't freak out, this is a self-signed cert for testing purposes only.
|
|
6
1
|
import forge from "node-forge";
|
|
7
2
|
|
|
3
|
+
const caName = "Pepr Ephemeral CA";
|
|
4
|
+
|
|
8
5
|
export interface TLSOut {
|
|
9
6
|
ca: string;
|
|
10
7
|
crt: string;
|
|
11
8
|
key: string;
|
|
9
|
+
pem: {
|
|
10
|
+
ca: string;
|
|
11
|
+
crt: string;
|
|
12
|
+
key: string;
|
|
13
|
+
};
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Generates a self-signed CA and server certificate with Subject Alternative Names (SANs) for the K8s webhook.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} name - The name to use for the server certificate's Common Name and SAN DNS entry.
|
|
20
|
+
* @returns {TLSOut} - An object containing the Base64-encoded CA, server certificate, and server private key.
|
|
21
|
+
*/
|
|
14
22
|
export function genTLS(name: string): TLSOut {
|
|
15
|
-
// Generate a new CA key pair
|
|
23
|
+
// Generate a new CA key pair and create a self-signed CA certificate
|
|
16
24
|
const caKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
17
|
-
const caCert =
|
|
18
|
-
|
|
19
|
-
caCert.
|
|
20
|
-
caCert.validity.notBefore = new Date();
|
|
21
|
-
caCert.validity.notAfter = new Date();
|
|
22
|
-
caCert.validity.notAfter.setFullYear(caCert.validity.notBefore.getFullYear() + 1);
|
|
23
|
-
const caAttrs = [
|
|
25
|
+
const caCert = genCert(caKeys, caName, [{ name: "commonName", value: caName }]);
|
|
26
|
+
|
|
27
|
+
caCert.setExtensions([
|
|
24
28
|
{
|
|
25
|
-
name: "
|
|
26
|
-
|
|
29
|
+
name: "basicConstraints",
|
|
30
|
+
cA: true,
|
|
27
31
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
{
|
|
33
|
+
name: "keyUsage",
|
|
34
|
+
keyCertSign: true,
|
|
35
|
+
digitalSignature: true,
|
|
36
|
+
nonRepudiation: true,
|
|
37
|
+
keyEncipherment: true,
|
|
38
|
+
dataEncipherment: true,
|
|
39
|
+
},
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// Generate a new server key pair and create a server certificate signed by the CA
|
|
43
|
+
const serverKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
44
|
+
const serverCert = genCert(serverKeys, name, caCert.subject.attributes);
|
|
45
|
+
|
|
46
|
+
// Sign both certificates with the CA private key
|
|
31
47
|
caCert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
48
|
+
serverCert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
32
49
|
|
|
33
|
-
//
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
50
|
+
// Convert the keys and certificates to PEM format
|
|
51
|
+
const pem = {
|
|
52
|
+
ca: forge.pki.certificateToPem(caCert),
|
|
53
|
+
crt: forge.pki.certificateToPem(serverCert),
|
|
54
|
+
key: forge.pki.privateKeyToPem(serverKeys.privateKey),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Base64-encode the PEM strings
|
|
58
|
+
const ca = Buffer.from(pem.ca).toString("base64");
|
|
59
|
+
const key = Buffer.from(pem.key).toString("base64");
|
|
60
|
+
const crt = Buffer.from(pem.crt).toString("base64");
|
|
61
|
+
|
|
62
|
+
return { ca, key, crt, pem };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function genCert(key: forge.pki.rsa.KeyPair, name: string, issuer: forge.pki.CertificateField[]) {
|
|
66
|
+
const crt = forge.pki.createCertificate();
|
|
67
|
+
crt.publicKey = key.publicKey;
|
|
68
|
+
crt.serialNumber = "01";
|
|
69
|
+
crt.validity.notBefore = new Date();
|
|
70
|
+
crt.validity.notAfter = new Date();
|
|
71
|
+
crt.validity.notAfter.setFullYear(crt.validity.notBefore.getFullYear() + 1);
|
|
72
|
+
|
|
73
|
+
// Add SANs to the server certificate
|
|
74
|
+
crt.setExtensions([
|
|
42
75
|
{
|
|
43
|
-
name: "
|
|
44
|
-
|
|
76
|
+
name: "subjectAltName",
|
|
77
|
+
altNames: [
|
|
78
|
+
{
|
|
79
|
+
type: 2, // DNS
|
|
80
|
+
value: name,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
45
83
|
},
|
|
46
|
-
];
|
|
47
|
-
cert.setSubject(attrs);
|
|
48
|
-
cert.setIssuer(caCert.subject.attributes);
|
|
49
|
-
cert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
84
|
+
]);
|
|
50
85
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
const key = Buffer.from(forge.pki.privateKeyToPem(keys.privateKey)).toString("base64");
|
|
54
|
-
const crt = Buffer.from(forge.pki.certificateToPem(cert)).toString("base64");
|
|
86
|
+
// Set the server certificate's issuer to the CA
|
|
87
|
+
crt.setIssuer(issuer);
|
|
55
88
|
|
|
56
|
-
return
|
|
89
|
+
return crt;
|
|
57
90
|
}
|
package/src/lib/k8s/webhook.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
AdmissionregistrationV1Api,
|
|
6
|
+
AdmissionregistrationV1WebhookClientConfig,
|
|
6
7
|
AppsV1Api,
|
|
7
8
|
CoreV1Api,
|
|
8
9
|
KubeConfig,
|
|
10
|
+
NetworkingV1Api,
|
|
9
11
|
RbacAuthorizationV1Api,
|
|
10
12
|
V1ClusterRole,
|
|
11
13
|
V1ClusterRoleBinding,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
V1LabelSelectorRequirement,
|
|
14
16
|
V1MutatingWebhookConfiguration,
|
|
15
17
|
V1Namespace,
|
|
18
|
+
V1NetworkPolicy,
|
|
16
19
|
V1Secret,
|
|
17
20
|
V1Service,
|
|
18
21
|
V1ServiceAccount,
|
|
@@ -31,17 +34,21 @@ const peprIgnore: V1LabelSelectorRequirement = {
|
|
|
31
34
|
|
|
32
35
|
export class Webhook {
|
|
33
36
|
private name: string;
|
|
34
|
-
private
|
|
37
|
+
private _tls: TLSOut;
|
|
35
38
|
|
|
36
39
|
public image: string;
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
public get tls(): TLSOut {
|
|
42
|
+
return this._tls;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
constructor(private readonly config: ModuleConfig, private readonly host?: string) {
|
|
39
46
|
this.name = `pepr-${config.uuid}`;
|
|
40
47
|
|
|
41
48
|
this.image = `ghcr.io/defenseunicorns/pepr/controller:${config.version}`;
|
|
42
49
|
|
|
43
50
|
// Generate the ephemeral tls things
|
|
44
|
-
this.
|
|
51
|
+
this._tls = genTLS(this.host || `${this.name}.pepr-system.svc`);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
/** Generate the pepr-system namespace */
|
|
@@ -117,8 +124,8 @@ export class Webhook {
|
|
|
117
124
|
},
|
|
118
125
|
type: "kubernetes.io/tls",
|
|
119
126
|
data: {
|
|
120
|
-
"tls.crt": this.
|
|
121
|
-
"tls.key": this.
|
|
127
|
+
"tls.crt": this._tls.crt,
|
|
128
|
+
"tls.key": this._tls.key,
|
|
122
129
|
},
|
|
123
130
|
};
|
|
124
131
|
}
|
|
@@ -136,6 +143,22 @@ export class Webhook {
|
|
|
136
143
|
});
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
const clientConfig: AdmissionregistrationV1WebhookClientConfig = {
|
|
147
|
+
caBundle: this._tls.ca,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// If a host is specified, use that with a port of 3000
|
|
151
|
+
if (this.host) {
|
|
152
|
+
clientConfig.url = `https://${this.host}:3000/mutate`;
|
|
153
|
+
} else {
|
|
154
|
+
// Otherwise, use the service
|
|
155
|
+
clientConfig.service = {
|
|
156
|
+
name: this.name,
|
|
157
|
+
namespace: "pepr-system",
|
|
158
|
+
path: "/mutate",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
return {
|
|
140
163
|
apiVersion: "admissionregistration.k8s.io/v1",
|
|
141
164
|
kind: "MutatingWebhookConfiguration",
|
|
@@ -144,14 +167,7 @@ export class Webhook {
|
|
|
144
167
|
{
|
|
145
168
|
name: `${name}.pepr.dev`,
|
|
146
169
|
admissionReviewVersions: ["v1", "v1beta1"],
|
|
147
|
-
clientConfig
|
|
148
|
-
caBundle: this.tls.ca,
|
|
149
|
-
service: {
|
|
150
|
-
name: this.name,
|
|
151
|
-
namespace: "pepr-system",
|
|
152
|
-
path: "/mutate",
|
|
153
|
-
},
|
|
154
|
-
},
|
|
170
|
+
clientConfig,
|
|
155
171
|
failurePolicy: "Ignore",
|
|
156
172
|
matchPolicy: "Equivalent",
|
|
157
173
|
timeoutSeconds: 15,
|
|
@@ -167,7 +183,7 @@ export class Webhook {
|
|
|
167
183
|
apiGroups: ["*"],
|
|
168
184
|
apiVersions: ["*"],
|
|
169
185
|
operations: ["CREATE", "UPDATE", "DELETE"],
|
|
170
|
-
resources: ["
|
|
186
|
+
resources: ["*/*"],
|
|
171
187
|
},
|
|
172
188
|
],
|
|
173
189
|
// @todo: track side effects state
|
|
@@ -189,7 +205,7 @@ export class Webhook {
|
|
|
189
205
|
},
|
|
190
206
|
},
|
|
191
207
|
spec: {
|
|
192
|
-
replicas:
|
|
208
|
+
replicas: 2,
|
|
193
209
|
selector: {
|
|
194
210
|
matchLabels: {
|
|
195
211
|
app: this.name,
|
|
@@ -237,6 +253,11 @@ export class Webhook {
|
|
|
237
253
|
mountPath: "/etc/certs",
|
|
238
254
|
readOnly: true,
|
|
239
255
|
},
|
|
256
|
+
{
|
|
257
|
+
name: "module",
|
|
258
|
+
mountPath: "/app/module.js.gz",
|
|
259
|
+
readOnly: true,
|
|
260
|
+
},
|
|
240
261
|
],
|
|
241
262
|
},
|
|
242
263
|
],
|
|
@@ -247,6 +268,12 @@ export class Webhook {
|
|
|
247
268
|
secretName: `${this.name}-tls`,
|
|
248
269
|
},
|
|
249
270
|
},
|
|
271
|
+
{
|
|
272
|
+
name: "module",
|
|
273
|
+
secret: {
|
|
274
|
+
secretName: `${this.name}-module`,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
250
277
|
],
|
|
251
278
|
},
|
|
252
279
|
},
|
|
@@ -254,6 +281,45 @@ export class Webhook {
|
|
|
254
281
|
};
|
|
255
282
|
}
|
|
256
283
|
|
|
284
|
+
/** Only permit the */
|
|
285
|
+
networkPolicy(): V1NetworkPolicy {
|
|
286
|
+
return {
|
|
287
|
+
apiVersion: "networking.k8s.io/v1",
|
|
288
|
+
kind: "NetworkPolicy",
|
|
289
|
+
metadata: {
|
|
290
|
+
name: this.name,
|
|
291
|
+
namespace: "pepr-system",
|
|
292
|
+
},
|
|
293
|
+
spec: {
|
|
294
|
+
podSelector: {
|
|
295
|
+
matchLabels: {
|
|
296
|
+
app: this.name,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
policyTypes: ["Ingress"],
|
|
300
|
+
ingress: [
|
|
301
|
+
{
|
|
302
|
+
from: [
|
|
303
|
+
{
|
|
304
|
+
namespaceSelector: {
|
|
305
|
+
matchLabels: {
|
|
306
|
+
"kubernetes.io/metadata.name": "kube-system",
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
ports: [
|
|
312
|
+
{
|
|
313
|
+
protocol: "TCP",
|
|
314
|
+
port: 443,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
257
323
|
service(): V1Service {
|
|
258
324
|
return {
|
|
259
325
|
apiVersion: "v1",
|
|
@@ -324,6 +390,7 @@ export class Webhook {
|
|
|
324
390
|
allYaml(code: string) {
|
|
325
391
|
const resources = [
|
|
326
392
|
this.namespace(),
|
|
393
|
+
this.networkPolicy(),
|
|
327
394
|
this.clusterRole(),
|
|
328
395
|
this.clusterRoleBinding(),
|
|
329
396
|
this.serviceAccount(),
|
|
@@ -351,6 +418,7 @@ export class Webhook {
|
|
|
351
418
|
const rbacApi = kubeConfig.makeApiClient(RbacAuthorizationV1Api);
|
|
352
419
|
const appsApi = kubeConfig.makeApiClient(AppsV1Api);
|
|
353
420
|
const admissionApi = kubeConfig.makeApiClient(AdmissionregistrationV1Api);
|
|
421
|
+
const networkApi = kubeConfig.makeApiClient(NetworkingV1Api);
|
|
354
422
|
|
|
355
423
|
const ns = this.namespace();
|
|
356
424
|
try {
|
|
@@ -362,6 +430,16 @@ export class Webhook {
|
|
|
362
430
|
await coreV1Api.createNamespace(ns);
|
|
363
431
|
}
|
|
364
432
|
|
|
433
|
+
const netpol = this.networkPolicy();
|
|
434
|
+
try {
|
|
435
|
+
logger.info("Checking for network policy");
|
|
436
|
+
await networkApi.readNamespacedNetworkPolicy(netpol.metadata.name, namespace);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
logger.debug(e.body);
|
|
439
|
+
logger.info("Creating network policy");
|
|
440
|
+
await networkApi.createNamespacedNetworkPolicy(namespace, netpol);
|
|
441
|
+
}
|
|
442
|
+
|
|
365
443
|
const wh = this.mutatingWebhook();
|
|
366
444
|
try {
|
|
367
445
|
logger.info("Creating mutating webhook");
|
|
@@ -410,6 +488,11 @@ export class Webhook {
|
|
|
410
488
|
await coreV1Api.createNamespacedServiceAccount(namespace, sa);
|
|
411
489
|
}
|
|
412
490
|
|
|
491
|
+
// If a host is specified, we don't need to deploy the rest of the resources
|
|
492
|
+
if (this.host) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
413
496
|
const mod = this.moduleSecret(code);
|
|
414
497
|
try {
|
|
415
498
|
logger.info("Creating module secret");
|