diff --git a/build-and-deploy.sh b/build-and-deploy.sh index 3578797d..ff1b3bd8 100755 --- a/build-and-deploy.sh +++ b/build-and-deploy.sh @@ -16,9 +16,9 @@ WRONGSECRETS_IMAGE=$(cat helm/wrongsecrets-ctf-party/values.yaml | yq '.wrongsec WRONGSECRETS_TAG=$(cat helm/wrongsecrets-ctf-party/values.yaml | yq '.wrongsecrets.tag') WEBTOP_IMAGE=$(cat helm/wrongsecrets-ctf-party/values.yaml | yq '.virtualdesktop.image') WEBTOP_TAG=$(cat helm/wrongsecrets-ctf-party/values.yaml | yq '.virtualdesktop.tag') -echo "doing workaround for selaed secrets" +echo "doing workaround for sealed secrets" helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets -helm install ws-sealedsecrets sealed-secrets/sealed-secrets --namespace kube-system +helm install ws-sealedsecrets sealed-secrets/sealed-secrets --namespace kube-system --set-string fullnameOverride=sealed-secrets-controller echo "Pulling in required images to actually run $WRONGSECRETS_IMAGE:$WRONGSECRETS_TAG & $WEBTOP_IMAGE:$WEBTOP_TAG." echo "If you see an authentication failure: pull them manually by the following 2 commands" echo "'docker pull $WRONGSECRETS_IMAGE:$WRONGSECRETS_TAG'" diff --git a/cleaner/Dockerfile b/cleaner/Dockerfile index 5c81949d..21c9ad32 100644 --- a/cleaner/Dockerfile +++ b/cleaner/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine as build +FROM node:20-alpine AS build RUN mkdir -p /home/app WORKDIR /home/app COPY package.json package-lock.json ./ diff --git a/wrongsecrets-balancer/package-lock.json b/wrongsecrets-balancer/package-lock.json index 51e8c1b1..061ec426 100644 --- a/wrongsecrets-balancer/package-lock.json +++ b/wrongsecrets-balancer/package-lock.json @@ -27,6 +27,7 @@ "joi": "^17.13.3", "lodash": "^4.17.21", "minimatch": "^10.0.1", + "node-forge": "^1.3.1", "on-finished": "^2.4.1", "prom-client": "^15.1.3", "winston": "^3.17.0" @@ -34,6 +35,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.14.0", + "@types/bcryptjs": "^2.4.6", "cookie-signature": "^1.2.2", "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.2.1", @@ -1708,6 +1710,13 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", @@ -5840,6 +5849,15 @@ } } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/wrongsecrets-balancer/package.json b/wrongsecrets-balancer/package.json index cbc25015..88387b02 100644 --- a/wrongsecrets-balancer/package.json +++ b/wrongsecrets-balancer/package.json @@ -29,6 +29,7 @@ "joi": "^17.13.3", "lodash": "^4.17.21", "minimatch": "^10.0.1", + "node-forge": "^1.3.1", "on-finished": "^2.4.1", "prom-client": "^15.1.3", "winston": "^3.17.0" @@ -36,6 +37,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.14.0", + "@types/bcryptjs": "^2.4.6", "cookie-signature": "^1.2.2", "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.2.1", diff --git a/wrongsecrets-balancer/src/kubernetes.js b/wrongsecrets-balancer/src/kubernetes.js index 6953d57e..e658c48a 100644 --- a/wrongsecrets-balancer/src/kubernetes.js +++ b/wrongsecrets-balancer/src/kubernetes.js @@ -8,6 +8,9 @@ const { NetworkingV1Api, } = require('@kubernetes/client-node'); +// Crypto library for sealed secret +const forge = require('node-forge'); + const kc = new KubeConfig(); kc.loadFromCluster(); @@ -129,7 +132,7 @@ const checkSealedSecretsController = async () => { const response = await safeApiCall( () => k8sAppsApi.readNamespacedDeployment({ - name: 'ws-sealedsecrets-sealed-secrets', + name: 'sealed-secrets-controller', namespace: 'kube-system', }), 'Check Sealed Secrets controller' @@ -251,6 +254,29 @@ const createChallenge33SecretForTeam = async (team) => { }); }; +/** + * @param {string} value - the value to be sealed + */ +const sealSecret = async (value) => { + const sealedSecretCert = await getSealedSecretsPublicKey(); + const cert = forge.pki.certificateFromPem(sealedSecretCert); + const key = cert.publicKey; + const encrypted = key.encrypt(value, 'RSA-OAEP', { + md: forge.md.sha256.create(), + mgf1: { md: forge.md.sha1.create() }, + }); + return Buffer.from(encrypted, 'binary').toString('base64'); +}; + +/** + * @param {string} team - The team name + * @param {string} value - The challenge 48 secret + */ +const createChallenge48SecretForTeam = async (team, value) => { + const secretValue = await sealSecret(value); + createSealedSecretForTeam(team, 'challenge48secret', { secret: secretValue }); +}; + /** * Create a SealedSecret in the team's namespace for secure secret management * @param {string} team - The team name @@ -259,8 +285,7 @@ const createChallenge33SecretForTeam = async (team) => { */ const createSealedSecretForTeam = async (team, secretName, secretData) => { try { - // Note: In production, you would seal the data using kubeseal CLI or the controller's public key - // For this example, we're creating a template that would need to be sealed externally + logger.info(`Secret name: ${secretName}, Secret data: ${JSON.stringify(secretData)}`); const sealedSecretManifest = { apiVersion: 'bitnami.com/v1alpha1', kind: 'SealedSecret', @@ -272,6 +297,9 @@ const createSealedSecretForTeam = async (team, secretName, secretData) => { 'app.kubernetes.io/instance': `wrongsecrets-${team}`, 'app.kubernetes.io/part-of': 'wrongsecrets-ctf-party', }, + annotations: { + 'sealedsecrets.bitnami.com/namespace-wide': 'true', + }, }, spec: { template: { @@ -282,10 +310,13 @@ const createSealedSecretForTeam = async (team, secretName, secretData) => { 'app.kubernetes.io/name': 'wrongsecrets', 'app.kubernetes.io/instance': `wrongsecrets-${team}`, }, + annotations: { + 'sealedsecrets.bitnami.com/namespace-wide': 'true', + }, }, type: 'Opaque', }, - encryptedData: secretData, // This should be pre-sealed data + encryptedData: secretData, }, }; @@ -305,31 +336,22 @@ const createSealedSecretForTeam = async (team, secretName, secretData) => { } }; -/** - * Create a sealed secret for challenge 33 specific to the team - * TODO: REPLACE WITH CHALLENGE 53 FOR ACTUAL SEALED SECRET - * @param {string} team - The team name - */ -const createSealedChallenge33SecretForTeam = async (team) => { - const secretName = 'challenge33'; - const secretData = { - // Note: These values should be sealed using kubeseal before deployment - answer: challenge33Value || 'default-challenge33-value', - }; - - return createSealedSecretForTeam(team, secretName, secretData); -}; - /** * Get the Sealed Secrets controller public key for sealing secrets */ const getSealedSecretsPublicKey = async () => { try { + const list = await k8sCoreApi.listNamespacedSecret({ + namespace: 'kube-system', + labelSelector: 'sealedsecrets.bitnami.com/sealed-secrets-key=active', + limit: 1, + }); + const secretName = list.items.map((secret) => secret.metadata.name).find((name) => name); const response = await k8sCoreApi.readNamespacedSecret({ - name: 'sealed-secrets-key', + name: secretName, namespace: 'kube-system', }); - return response.data['tls.crt']; + return Buffer.from(response.data['tls.crt'], 'base64').toString('utf-8'); } catch (error) { logger.error('Failed to get Sealed Secrets public key:', error.body || error); throw new Error(`Failed to get public key: ${error.message}`); @@ -557,14 +579,6 @@ const deleteChallenge53DeploymentForTeam = async (team) => { * Enhanced deployment creation with SealedSecret integration */ const createK8sDeploymentForTeam = async ({ team, passcodeHash }) => { - // Check if we should use SealedSecrets - const useSealedSecrets = await checkSealedSecretsController(); - - if (useSealedSecrets) { - // Create sealed secrets for the team - await createSealedChallenge33SecretForTeam(team); - } - const deploymentWrongSecretsConfig = { metadata: { namespace: `t-${team}`, @@ -2517,13 +2531,13 @@ module.exports = { createSecretsfileForTeam, createChallenge33SecretForTeam, createSealedSecretForTeam, - createSealedChallenge33SecretForTeam, getSealedSecretsPublicKey, createNameSpaceForTeam, createK8sDeploymentForTeam, createK8sChallenge53DeploymentForTeam, getChallenge53InstanceForTeam, deleteChallenge53DeploymentForTeam, + createChallenge48SecretForTeam, // AWS functions createAWSSecretsProviderForTeam, diff --git a/wrongsecrets-balancer/src/teams/teams.js b/wrongsecrets-balancer/src/teams/teams.js index 9cb4124b..f88460d3 100644 --- a/wrongsecrets-balancer/src/teams/teams.js +++ b/wrongsecrets-balancer/src/teams/teams.js @@ -9,6 +9,8 @@ const promClient = require('prom-client'); const accessPassword = process.env.REACT_APP_ACCESS_PASSWORD; const hmac_key = process.env.REACT_APP_CREATE_TEAM_HMAC_KEY || 'hardcodedkey'; +const challenge48secret = cryptoRandomString({ length: 32 }).toUpperCase(); + const validator = expressJoiValidation.createValidator(); const k8sEnv = process.env.K8S_ENV || 'k8s'; const router = express.Router(); @@ -40,6 +42,7 @@ const { createRoleBindingForWebtop, createNSPsforTeam, createK8sChallenge53DeploymentForTeam, + createChallenge48SecretForTeam, } = require('../kubernetes'); const loginCounter = new promClient.Counter({ @@ -300,6 +303,15 @@ async function createTeam(req, res) { logger.error(`Error while creating challenge33 secretsfile ${team}: ${error}`); res.status(500).send({ message: 'Failed to Create Instance' }); } + + try { + logger.info(`Creating challenge48 secret for team '${team}'`); + await createChallenge48SecretForTeam(team, challenge48secret); + } catch (error) { + logger.error(`Error while creating challenge48 secret ${team}: ${error}`); + res.status(500).send({ message: 'Failed to Create Instance' }); + } + try { logger.info(`Creating WrongSecrets Deployment for team '${team}' with k8s (no cloud)`); await createK8sDeploymentForTeam({ team, passcodeHash: hash });