diff --git a/backend/package-lock.json b/backend/package-lock.json index 27ee940d3..b3eef6bd1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-elasticache": "^3.637.0", "@aws-sdk/client-iam": "^3.525.0", "@aws-sdk/client-kms": "^3.609.0", "@aws-sdk/client-secrets-manager": "^3.504.0", @@ -352,6 +353,309 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-elasticache": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.637.0.tgz", + "integrity": "sha512-e54OYm33DqmcsVHr1l+Eudt5d9PqcjDDJdQHLJrNGdrUkwmpuqnw3czkGjD5IP34XkcpQ5Gs1DSRAp07E8Zglw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/client-sts": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/client-sso": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.637.0.tgz", + "integrity": "sha512-+KjLvgX5yJYROWo3TQuwBJlHCY0zz9PsLuEolmXQn0BVK1L/m9GteZHtd+rEdAoDGBpE0Xqjy1oz5+SmtsaRUw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.637.0.tgz", + "integrity": "sha512-27bHALN6Qb6m6KZmPvRieJ/QRlj1lyac/GT2Rn5kJpre8Mpp+yxrtvp3h9PjNBty4lCeFEENfY4dGNSozBuBcw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/client-sts": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.637.0.tgz", + "integrity": "sha512-xUi7x4qDubtA8QREtlblPuAcn91GS/09YVEY/RwU7xCY0aqGuFwgszAANlha4OUIqva8oVj2WO4gJuG+iaSnhw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.637.0", + "@aws-sdk/core": "3.635.0", + "@aws-sdk/credential-provider-node": "3.637.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.637.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.4.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.15", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.2.0", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.15", + "@smithy/util-defaults-mode-node": "^3.0.15", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.637.0.tgz", + "integrity": "sha512-h+PFCWfZ0Q3Dx84SppET/TFpcQHmxFW8/oV9ArEvMilw4EBN+IlxgbL0CnHwjHW64szcmrM0mbebjEfHf4FXmw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.635.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.637.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.637.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.637.0.tgz", + "integrity": "sha512-yoEhoxJJfs7sPVQ6Is939BDQJZpZCoUgKr/ySse4YKOZ24t4VqgHA6+wV7rYh+7IW24Rd91UTvEzSuHYTlxlNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.635.0", + "@aws-sdk/credential-provider-ini": "3.637.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.637.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.637.0.tgz", + "integrity": "sha512-Mvz+h+e62/tl+dVikLafhv+qkZJ9RUb8l2YN/LeKMWkxQylPT83CPk9aimVhCV89zth1zpREArl97+3xsfgQvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.637.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.637.0.tgz", + "integrity": "sha512-EYo0NE9/da/OY8STDsK2LvM4kNa79DBsf4YVtaG4P5pZ615IeFsD8xOHZeuJmUrSMlVQ8ywPRX7WMucUybsKug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.637.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-elasticache/node_modules/@aws-sdk/util-endpoints": { + "version": "3.637.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.637.0.tgz", + "integrity": "sha512-pAqOKUHeVWHEXXDIp/qoMk/6jyxIb6GGjnK1/f8dKHtKIEs4tKsnnL563gceEvdad53OPXIt86uoevCcCzmBnw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-iam": { "version": "3.635.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.635.0.tgz", diff --git a/backend/package.json b/backend/package.json index 043394769..86d92c086 100644 --- a/backend/package.json +++ b/backend/package.json @@ -106,6 +106,7 @@ "vitest": "^1.2.2" }, "dependencies": { + "@aws-sdk/client-elasticache": "^3.637.0", "@aws-sdk/client-iam": "^3.525.0", "@aws-sdk/client-kms": "^3.609.0", "@aws-sdk/client-secrets-manager": "^3.504.0", diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index f181d1c23..49a11691c 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -7,6 +7,7 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; +import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; @@ -36,7 +37,6 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; -import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; diff --git a/backend/src/server/routes/est/certificate-est-router.ts b/backend/src/ee/routes/est/certificate-est-router.ts similarity index 96% rename from backend/src/server/routes/est/certificate-est-router.ts rename to backend/src/ee/routes/est/certificate-est-router.ts index 1eec348ce..7f401216e 100644 --- a/backend/src/server/routes/est/certificate-est-router.ts +++ b/backend/src/ee/routes/est/certificate-est-router.ts @@ -42,6 +42,11 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) = await res.hijack(); + // definitive connection timeout to clean-up open connections and prevent memory leak + res.raw.setTimeout(10 * 1000, () => { + res.raw.end(); + }); + res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`); res.raw.setHeader("Content-Length", 0); res.raw.statusCode = 401; diff --git a/backend/src/services/certificate-est/certificate-est-fns.ts b/backend/src/ee/services/certificate-est/certificate-est-fns.ts similarity index 100% rename from backend/src/services/certificate-est/certificate-est-fns.ts rename to backend/src/ee/services/certificate-est/certificate-est-fns.ts diff --git a/backend/src/services/certificate-est/certificate-est-service.ts b/backend/src/ee/services/certificate-est/certificate-est-service.ts similarity index 77% rename from backend/src/services/certificate-est/certificate-est-service.ts rename to backend/src/ee/services/certificate-est/certificate-est-service.ts index e85d4658b..b8e179856 100644 --- a/backend/src/services/certificate-est/certificate-est-service.ts +++ b/backend/src/ee/services/certificate-est/certificate-est-service.ts @@ -1,16 +1,17 @@ import * as x509 from "@peculiar/x509"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { isCertChainValid } from "@app/services/certificate/certificate-fns"; +import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns"; +import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; +import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { isCertChainValid } from "../certificate/certificate-fns"; -import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal"; -import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; -import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns"; -import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service"; -import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; -import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { TProjectDALFactory } from "../project/project-dal"; +import { TLicenseServiceFactory } from "../license/license-service"; import { convertRawCertsToPkcs7 } from "./certificate-est-fns"; type TCertificateEstServiceFactoryDep = { @@ -21,6 +22,7 @@ type TCertificateEstServiceFactoryDep = { certificateAuthorityCertDAL: Pick; projectDAL: Pick; kmsService: Pick; + licenseService: Pick; }; export type TCertificateEstServiceFactory = ReturnType; @@ -32,7 +34,8 @@ export const certificateEstServiceFactory = ({ certificateAuthorityCertDAL, certificateAuthorityDAL, projectDAL, - kmsService + kmsService, + licenseService }: TCertificateEstServiceFactoryDep) => { const simpleReenroll = async ({ csr, @@ -48,6 +51,14 @@ export const certificateEstServiceFactory = ({ certificateTemplateId }); + const plan = await licenseService.getPlan(estConfig.orgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: + "Failed to perform EST operation - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan." + }); + } + if (!estConfig.isEnabled) { throw new BadRequestError({ message: "EST is disabled" @@ -146,6 +157,14 @@ export const certificateEstServiceFactory = ({ certificateTemplateId }); + const plan = await licenseService.getPlan(estConfig.orgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: + "Failed to perform EST operation - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan." + }); + } + if (!estConfig.isEnabled) { throw new BadRequestError({ message: "EST is disabled" @@ -196,6 +215,24 @@ export const certificateEstServiceFactory = ({ }); } + const estConfig = await certificateTemplateService.getEstConfiguration({ + isInternal: true, + certificateTemplateId + }); + + const plan = await licenseService.getPlan(estConfig.orgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: "Failed to perform EST operation - caCerts due to plan restriction. Upgrade to the Enterprise plan." + }); + } + + if (!estConfig.isEnabled) { + throw new BadRequestError({ + message: "EST is disabled" + }); + } + const ca = await certificateAuthorityDAL.findById(certTemplate.caId); if (!ca) { throw new NotFoundError({ diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts new file mode 100644 index 000000000..2cb862029 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/aws-elasticache.ts @@ -0,0 +1,226 @@ +import { + CreateUserCommand, + CreateUserGroupCommand, + DeleteUserCommand, + DescribeReplicationGroupsCommand, + DescribeUserGroupsCommand, + ElastiCache, + ModifyReplicationGroupCommand, + ModifyUserGroupCommand +} from "@aws-sdk/client-elasticache"; +import handlebars from "handlebars"; +import { customAlphabet } from "nanoid"; +import { z } from "zod"; + +import { BadRequestError } from "@app/lib/errors"; + +import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models"; + +const CreateElastiCacheUserSchema = z.object({ + UserId: z.string().trim().min(1), + UserName: z.string().trim().min(1), + Engine: z.string().default("redis"), + Passwords: z.array(z.string().trim().min(1)).min(1).max(1), // Minimum password length is 16 characters, required by AWS. + AccessString: z.string().trim().min(1) // Example: "on ~* +@all" +}); + +const DeleteElasticCacheUserSchema = z.object({ + UserId: z.string().trim().min(1) +}); + +type TElastiCacheRedisUser = { userId: string; password: string }; +type TBasicAWSCredentials = { accessKeyId: string; secretAccessKey: string }; + +type TCreateElastiCacheUserInput = z.infer; +type TDeleteElastiCacheUserInput = z.infer; + +const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => { + const elastiCache = new ElastiCache({ + region, + credentials + }); + const infisicalGroup = "infisical-managed-group-elasticache"; + + const ensureInfisicalGroupExists = async (clusterName: string) => { + const replicationGroups = await elastiCache.send(new DescribeUserGroupsCommand()); + + const existingGroup = replicationGroups.UserGroups?.find((group) => group.UserGroupId === infisicalGroup); + + let newlyCreatedGroup = false; + if (!existingGroup) { + const createGroupCommand = new CreateUserGroupCommand({ + UserGroupId: infisicalGroup, + UserIds: ["default"], + Engine: "redis" + }); + + await elastiCache.send(createGroupCommand); + newlyCreatedGroup = true; + } + + if (existingGroup || newlyCreatedGroup) { + const replicationGroup = ( + await elastiCache.send( + new DescribeReplicationGroupsCommand({ + ReplicationGroupId: clusterName + }) + ) + ).ReplicationGroups?.[0]; + + if (!replicationGroup?.UserGroupIds?.includes(infisicalGroup)) { + // If the replication group doesn't have the infisical user group, we need to associate it + const modifyGroupCommand = new ModifyReplicationGroupCommand({ + UserGroupIdsToAdd: [infisicalGroup], + UserGroupIdsToRemove: [], + ApplyImmediately: true, + ReplicationGroupId: clusterName + }); + await elastiCache.send(modifyGroupCommand); + } + } + }; + + const addUserToInfisicalGroup = async (userId: string) => { + // figure out if the default user is already in the group, if it is, then we shouldn't add it again + + const addUserToGroupCommand = new ModifyUserGroupCommand({ + UserGroupId: infisicalGroup, + UserIdsToAdd: [userId], + UserIdsToRemove: [] + }); + + await elastiCache.send(addUserToGroupCommand); + }; + + const createUser = async (creationInput: TCreateElastiCacheUserInput, clusterName: string) => { + await ensureInfisicalGroupExists(clusterName); + + await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user + await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists() + + return { + userId: creationInput.UserId, + password: creationInput.Passwords[0] + }; + }; + + const deleteUser = async ( + deletionInput: TDeleteElastiCacheUserInput + ): Promise> => { + await elastiCache.send(new DeleteUserCommand(deletionInput)); + return { userId: deletionInput.UserId }; + }; + + const verifyCredentials = async (clusterName: string) => { + await elastiCache.send( + new DescribeReplicationGroupsCommand({ + ReplicationGroupId: clusterName + }) + ); + }; + + return { + createUser, + deleteUser, + verifyCredentials + }; +}; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +const generateUsername = () => { + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"; + return `inf-${customAlphabet(charset, 32)()}`; // Username must start with an ascii letter, so we prepend the username with "inf-" +}; + +export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const providerInputs = DynamicSecretAwsElastiCacheSchema.parse(inputs); + + // We need to ensure the that the creation & revocation statements are valid and can be used to create and revoke users. + // We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements. + CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement)); + DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement)); + + return providerInputs; + }; + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + await ElastiCacheUserManager( + { + accessKeyId: providerInputs.accessKeyId, + secretAccessKey: providerInputs.secretAccessKey + }, + providerInputs.region + ).verifyCredentials(providerInputs.clusterName); + return true; + }; + + const create = async (inputs: unknown, expireAt: number) => { + const providerInputs = await validateProviderInputs(inputs); + if (!(await validateConnection(providerInputs))) { + throw new BadRequestError({ message: "Failed to establish connection" }); + } + + const leaseUsername = generateUsername(); + const leasePassword = generatePassword(); + const leaseExpiration = new Date(expireAt).toISOString(); + + const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ + username: leaseUsername, + password: leasePassword, + expiration: leaseExpiration + }); + + const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement)); + + await ElastiCacheUserManager( + { + accessKeyId: providerInputs.accessKeyId, + secretAccessKey: providerInputs.secretAccessKey + }, + providerInputs.region + ).createUser(parsedStatement, providerInputs.clusterName); + + return { + entityId: leaseUsername, + data: { + DB_USERNAME: leaseUsername, + DB_PASSWORD: leasePassword + } + }; + }; + + const revoke = async (inputs: unknown, entityId: string) => { + const providerInputs = await validateProviderInputs(inputs); + + const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId }); + const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement)); + + await ElastiCacheUserManager( + { + accessKeyId: providerInputs.accessKeyId, + secretAccessKey: providerInputs.secretAccessKey + }, + providerInputs.region + ).deleteUser(parsedStatement); + + return { entityId }; + }; + + const renew = async (inputs: unknown, entityId: string) => { + // Do nothing + return { entityId }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index beb6c428e..1ee020625 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,10 +1,14 @@ +import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; import { CassandraProvider } from "./cassandra"; import { DynamicSecretProviders } from "./models"; +import { RedisDatabaseProvider } from "./redis"; import { SqlDatabaseProvider } from "./sql-database"; export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(), [DynamicSecretProviders.Cassandra]: CassandraProvider(), - [DynamicSecretProviders.AwsIam]: AwsIamProvider() + [DynamicSecretProviders.AwsIam]: AwsIamProvider(), + [DynamicSecretProviders.Redis]: RedisDatabaseProvider(), + [DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 14b79eeea..60cab6f80 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -7,6 +7,29 @@ export enum SqlProviders { MsSQL = "mssql" } +export const DynamicSecretRedisDBSchema = z.object({ + host: z.string().trim().toLowerCase(), + port: z.number(), + username: z.string().trim(), // this is often "default". + password: z.string().trim().optional(), + + creationStatement: z.string().trim(), + revocationStatement: z.string().trim(), + renewStatement: z.string().trim().optional(), + ca: z.string().optional() +}); + +export const DynamicSecretAwsElastiCacheSchema = z.object({ + clusterName: z.string().trim().min(1), + accessKeyId: z.string().trim().min(1), + secretAccessKey: z.string().trim().min(1), + + region: z.string().trim(), + creationStatement: z.string().trim(), + revocationStatement: z.string().trim(), + ca: z.string().optional() +}); + export const DynamicSecretSqlDBSchema = z.object({ client: z.nativeEnum(SqlProviders), host: z.string().trim().toLowerCase(), @@ -47,13 +70,17 @@ export const DynamicSecretAwsIamSchema = z.object({ export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", - AwsIam = "aws-iam" + AwsIam = "aws-iam", + Redis = "redis", + AwsElastiCache = "aws-elasticache" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }), - z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }) + z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }), + z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }), + z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/dynamic-secret/providers/redis.ts b/backend/src/ee/services/dynamic-secret/providers/redis.ts new file mode 100644 index 000000000..23eb454c5 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/redis.ts @@ -0,0 +1,183 @@ +/* eslint-disable no-console */ +import handlebars from "handlebars"; +import { Redis } from "ioredis"; +import { customAlphabet } from "nanoid"; +import { z } from "zod"; + +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; +import { getDbConnectionHost } from "@app/lib/knex"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; + +import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models"; + +const generatePassword = () => { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#"; + return customAlphabet(charset, 64)(); +}; + +const generateUsername = () => { + return alphaNumericNanoId(32); +}; + +const executeTransactions = async (connection: Redis, commands: string[]): Promise<(string | null)[] | null> => { + // Initiate a transaction + const pipeline = connection.multi(); + + // Add all commands to the pipeline + for (const command of commands) { + const args = command + .split(" ") + .map((arg) => arg.trim()) + .filter((arg) => arg.length > 0); + pipeline.call(args[0], ...args.slice(1)); + } + + // Execute the transaction + const results = await pipeline.exec(); + + if (!results) { + throw new BadRequestError({ message: "Redis transaction failed: No results returned" }); + } + + // Check for errors in the results + const errors = results.filter(([err]) => err !== null); + if (errors.length > 0) { + throw new BadRequestError({ message: "Redis transaction failed with errors" }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return results.map(([_, result]) => result as string | null); +}; + +export const RedisDatabaseProvider = (): TDynamicProviderFns => { + const validateProviderInputs = async (inputs: unknown) => { + const appCfg = getConfig(); + const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not + const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI); + + const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs); + if ( + isCloud && + // localhost + // internal ips + (providerInputs.host === "host.docker.internal" || + providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) || + providerInputs.host.match(/^192\.168\.\d+\.\d+/)) + ) + throw new BadRequestError({ message: "Invalid db host" }); + if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host) + throw new BadRequestError({ message: "Invalid db host" }); + return providerInputs; + }; + + const getClient = async (providerInputs: z.infer) => { + let connection: Redis | null = null; + try { + connection = new Redis({ + username: providerInputs.username, + host: providerInputs.host, + port: providerInputs.port, + password: providerInputs.password, + ...(providerInputs.ca && { + tls: { + rejectUnauthorized: false, + ca: providerInputs.ca + } + }) + }); + + let result: string; + if (providerInputs.password) { + result = await connection.auth(providerInputs.username, providerInputs.password, () => {}); + } else { + result = await connection.auth(providerInputs.username, () => {}); + } + + if (result !== "OK") { + throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` }); + } + + return connection; + } catch (err) { + if (connection) await connection.quit(); + + throw err; + } + }; + + const validateConnection = async (inputs: unknown) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const pingResponse = await connection + .ping() + .then(() => true) + .catch(() => false); + + return pingResponse; + }; + + const create = async (inputs: unknown, expireAt: number) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const username = generateUsername(); + const password = generatePassword(); + const expiration = new Date(expireAt).toISOString(); + + const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({ + username, + password, + expiration + }); + + const queries = creationStatement.toString().split(";").filter(Boolean); + + await executeTransactions(connection, queries); + + await connection.quit(); + return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } }; + }; + + const revoke = async (inputs: unknown, entityId: string) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const username = entityId; + + const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username }); + const queries = revokeStatement.toString().split(";").filter(Boolean); + + await executeTransactions(connection, queries); + + await connection.quit(); + return { entityId: username }; + }; + + const renew = async (inputs: unknown, entityId: string, expireAt: number) => { + const providerInputs = await validateProviderInputs(inputs); + const connection = await getClient(providerInputs); + + const username = entityId; + const expiration = new Date(expireAt).toISOString(); + + const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration }); + + if (renewStatement) { + const queries = renewStatement.toString().split(";").filter(Boolean); + await executeTransactions(connection, queries); + } + + await connection.quit(); + return { entityId: username }; + }; + + return { + validateProviderInputs, + validateConnection, + create, + revoke, + renew + }; +}; diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index bd40f75cb..beb981a5a 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -45,7 +45,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ readLimit: 60, writeLimit: 200, secretsLimit: 40 - } + }, + pkiEst: false }); export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 70db8e4d9..6e671c26f 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -63,6 +63,7 @@ export type TFeatureSet = { writeLimit: number; secretsLimit: number; }; + pkiEst: boolean; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts index 01b2451ac..05b7280b2 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts @@ -36,9 +36,7 @@ export const sendApprovalEmailsFn = async ({ firstName: reviewerUser.firstName, projectName: project.name, organizationName: project.organization.name, - approvalUrl: `${cfg.isDevelopmentMode ? "https" : "http"}://${cfg.SITE_URL}/project/${ - project.id - }/approval?requestId=${secretApprovalRequest.id}` + approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}` }, template: SmtpTemplates.SecretApprovalRequestNeedsReview }); diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index ca19fd425..f21312311 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -1,6 +1,7 @@ import { Logger } from "pino"; import { z } from "zod"; +import { removeTrailingSlash } from "../fn"; import { zpStr } from "../zod"; export const GITLAB_URL = "https://gitlab.com"; @@ -63,7 +64,9 @@ const envSchema = z .string() .min(32) .default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"), - SITE_URL: zpStr(z.string().optional()), + + // Ensure that the SITE_URL never ends with a trailing slash + SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional(), // Telemetry TELEMETRY_ENABLED: zodStrBool.default("true"), POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")), diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 5d20f1cff..f33456bd5 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1,7 +1,9 @@ import { CronJob } from "cron"; +import { Redis } from "ioredis"; import { Knex } from "knex"; import { z } from "zod"; +import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router"; import { registerV1EERoutes } from "@app/ee/routes/v1"; import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; @@ -16,6 +18,7 @@ import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audi import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service"; +import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service"; import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers"; @@ -71,6 +74,7 @@ import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal" import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue"; @@ -90,7 +94,6 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; -import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; @@ -197,7 +200,6 @@ import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectPermission } from "../plugins/auth/inject-permission"; import { injectRateLimits } from "../plugins/inject-rate-limits"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; -import { registerCertificateEstRouter } from "./est/certificate-est-router"; import { registerV1Routes } from "./v1"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; @@ -665,7 +667,8 @@ export const registerRoutes = async ( certificateAuthorityDAL, permissionService, kmsService, - projectDAL + projectDAL, + licenseService }); const certificateEstService = certificateEstServiceFactory({ @@ -675,7 +678,8 @@ export const registerRoutes = async ( certificateAuthorityCertDAL, certificateAuthorityDAL, projectDAL, - kmsService + kmsService, + licenseService }); const pkiAlertService = pkiAlertServiceFactory({ @@ -1257,7 +1261,7 @@ export const registerRoutes = async ( response: { 200: z.object({ date: z.date(), - message: z.literal("Ok"), + message: z.string().optional(), emailConfigured: z.boolean().optional(), inviteOnlySignup: z.boolean().optional(), redisConfigured: z.boolean().optional(), @@ -1266,12 +1270,37 @@ export const registerRoutes = async ( }) } }, - handler: async () => { + handler: async (request, reply) => { const cfg = getConfig(); const serverCfg = await getServerCfg(); + + try { + await db.raw("SELECT NOW()"); + } catch (err) { + logger.error("Health check: database connection failed", err); + return reply.code(503).send({ + date: new Date(), + message: "Service unavailable" + }); + } + + if (cfg.isRedisConfigured) { + const redis = new Redis(cfg.REDIS_URL); + try { + await redis.ping(); + redis.disconnect(); + } catch (err) { + logger.error("Health check: redis connection failed", err); + return reply.code(503).send({ + date: new Date(), + message: "Service unavailable" + }); + } + } + return { date: new Date(), - message: "Ok" as const, + message: "Ok", emailConfigured: cfg.isSmtpConfigured, inviteOnlySignup: Boolean(serverCfg.allowSignUp), redisConfigured: cfg.isRedisConfigured, diff --git a/backend/src/services/certificate-template/certificate-template-dal.ts b/backend/src/services/certificate-template/certificate-template-dal.ts index a30c4890d..c500833d1 100644 --- a/backend/src/services/certificate-template/certificate-template-dal.ts +++ b/backend/src/services/certificate-template/certificate-template-dal.ts @@ -1,3 +1,5 @@ +import { Knex } from "knex"; + import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; @@ -30,20 +32,21 @@ export const certificateTemplateDALFactory = (db: TDbClient) => { } }; - const getById = async (id: string) => { + const getById = async (id: string, tx?: Knex) => { try { - const certTemplate = await db - .replicaNode()(TableName.CertificateTemplate) + const certTemplate = await (tx || db.replicaNode())(TableName.CertificateTemplate) .join( TableName.CertificateAuthority, `${TableName.CertificateAuthority}.id`, `${TableName.CertificateTemplate}.caId` ) + .join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`) .where(`${TableName.CertificateTemplate}.id`, "=", id) .select(selectAllTableCols(TableName.CertificateTemplate)) .select( db.ref("projectId").withSchema(TableName.CertificateAuthority), - db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority) + db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority), + db.ref("orgId").withSchema(TableName.Project) ) .first(); diff --git a/backend/src/services/certificate-template/certificate-template-service.ts b/backend/src/services/certificate-template/certificate-template-service.ts index 31745e752..2a8134093 100644 --- a/backend/src/services/certificate-template/certificate-template-service.ts +++ b/backend/src/services/certificate-template/certificate-template-service.ts @@ -3,6 +3,7 @@ import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; @@ -32,6 +33,7 @@ type TCertificateTemplateServiceFactoryDep = { kmsService: Pick; certificateAuthorityDAL: Pick; permissionService: Pick; + licenseService: Pick; }; export type TCertificateTemplateServiceFactory = ReturnType; @@ -42,7 +44,8 @@ export const certificateTemplateServiceFactory = ({ certificateAuthorityDAL, permissionService, kmsService, - projectDAL + projectDAL, + licenseService }: TCertificateTemplateServiceFactoryDep) => { const createCertTemplate = async ({ caId, @@ -75,23 +78,28 @@ export const certificateTemplateServiceFactory = ({ ProjectPermissionSub.CertificateTemplates ); - const { id } = await certificateTemplateDAL.create({ - caId, - pkiCollectionId, - name, - commonName, - subjectAlternativeName, - ttl + return certificateTemplateDAL.transaction(async (tx) => { + const { id } = await certificateTemplateDAL.create( + { + caId, + pkiCollectionId, + name, + commonName, + subjectAlternativeName, + ttl + }, + tx + ); + + const certificateTemplate = await certificateTemplateDAL.getById(id, tx); + if (!certificateTemplate) { + throw new NotFoundError({ + message: "Certificate template not found" + }); + } + + return certificateTemplate; }); - - const certificateTemplate = await certificateTemplateDAL.getById(id); - if (!certificateTemplate) { - throw new NotFoundError({ - message: "Certificate template not found" - }); - } - - return certificateTemplate; }; const updateCertTemplate = async ({ @@ -136,23 +144,29 @@ export const certificateTemplateServiceFactory = ({ } } - await certificateTemplateDAL.updateById(certTemplate.id, { - caId, - pkiCollectionId, - commonName, - subjectAlternativeName, - name, - ttl + return certificateTemplateDAL.transaction(async (tx) => { + await certificateTemplateDAL.updateById( + certTemplate.id, + { + caId, + pkiCollectionId, + commonName, + subjectAlternativeName, + name, + ttl + }, + tx + ); + + const updatedTemplate = await certificateTemplateDAL.getById(id, tx); + if (!updatedTemplate) { + throw new NotFoundError({ + message: "Certificate template not found" + }); + } + + return updatedTemplate; }); - - const updatedTemplate = await certificateTemplateDAL.getById(id); - if (!updatedTemplate) { - throw new NotFoundError({ - message: "Certificate template not found" - }); - } - - return updatedTemplate; }; const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => { @@ -215,6 +229,13 @@ export const certificateTemplateServiceFactory = ({ actor, actorOrgId }: TCreateEstConfigurationDTO) => { + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: "Failed to create EST configuration due to plan restriction. Upgrade to the Enterprise plan." + }); + } + const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certTemplate) { throw new NotFoundError({ @@ -285,6 +306,13 @@ export const certificateTemplateServiceFactory = ({ actor, actorOrgId }: TUpdateEstConfigurationDTO) => { + const plan = await licenseService.getPlan(actorOrgId); + if (!plan.pkiEst) { + throw new BadRequestError({ + message: "Failed to update EST configuration due to plan restriction. Upgrade to the Enterprise plan." + }); + } + const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId); if (!certTemplate) { throw new NotFoundError({ @@ -416,7 +444,8 @@ export const certificateTemplateServiceFactory = ({ isEnabled: estConfig.isEnabled, caChain: decryptedCaChain.toString(), hashedPassphrase: estConfig.hashedPassphrase, - projectId: certTemplate.projectId + projectId: certTemplate.projectId, + orgId: certTemplate.orgId }; }; diff --git a/cli/agent-config.yaml b/cli/agent-config.yaml index e767fdca5..210c21413 100644 --- a/cli/agent-config.yaml +++ b/cli/agent-config.yaml @@ -11,12 +11,25 @@ sinks: config: path: "access-token" templates: - - source-path: my-dot-ev-secret-template + - template-content: | + {{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }} + {{- range . }} + {{ .Key }}={{ .Value }} + {{- end }} + {{- end }} + destination-path: my-dot-env-0.env + config: + polling-interval: 60s + execute: + command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d + + - base64-template-content: e3stIHdpdGggc2VjcmV0ICIyMDJmMDRkNy1lNGNiLTQzZDQtYTI5Mi1lODkzNzEyZDYxZmMiICJkZXYiICIvIiB9fQp7ey0gcmFuZ2UgLiB9fQp7eyAuS2V5IH19PXt7IC5WYWx1ZSB9fQp7ey0gZW5kIH19Cnt7LSBlbmQgfX0= destination-path: my-dot-env.env config: polling-interval: 60s execute: command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d + - source-path: my-dot-ev-secret-template1 destination-path: my-dot-env-1.env config: diff --git a/cli/go.mod b/cli/go.mod index df902ee8d..bcde660ab 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -12,7 +12,7 @@ require ( github.com/gitleaks/go-gitdiff v0.8.0 github.com/h2non/filetype v1.1.3 github.com/infisical/go-sdk v0.3.3 - github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-isatty v0.0.18 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/mango-cobra v1.2.0 github.com/muesli/reflow v0.3.0 diff --git a/cli/go.sum b/cli/go.sum index 157c9f4b8..d6f04aed0 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -297,8 +297,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index 0c99377ae..118aca119 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -95,6 +95,7 @@ type Template struct { SourcePath string `yaml:"source-path"` Base64TemplateContent string `yaml:"base64-template-content"` DestinationPath string `yaml:"destination-path"` + TemplateContent string `yaml:"template-content"` Config struct { // Configurations for the template PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret @@ -432,6 +433,30 @@ func ProcessBase64Template(templateId int, encodedTemplate string, data interfac return &buf, nil } +func ProcessLiteralTemplate(templateId int, templateString string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) { + secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this + dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId) + funcs := template.FuncMap{ + "secret": secretFunction, + "dynamic_secret": dynamicSecretFunction, + } + + templateName := "literalTemplate" + + tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return nil, err + } + + return &buf, nil +} + + type AgentManager struct { accessToken string accessTokenTTL time.Duration @@ -820,6 +845,8 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId if secretTemplate.SourcePath != "" { processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases) + } else if secretTemplate.TemplateContent != "" { + processedTemplate, err = ProcessLiteralTemplate(templateId, secretTemplate.TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases) } else { processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases) } diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 1bad5e327..c4bbd0175 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -104,12 +104,6 @@ type GetAllSecretsParameters struct { Recursive bool } -type InjectableEnvironmentResult struct { - Variables []string - ETag string - SecretsCount int -} - type GetAllFoldersParameters struct { WorkspaceId string Environment string diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index b758ebd9d..69a310efa 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/sha256" "encoding/base64" - "encoding/hex" "fmt" "math/rand" "os" @@ -299,16 +298,3 @@ func GenerateRandomString(length int) string { } return string(b) } - -func GenerateETagFromSecrets(secrets []models.SingleEnvironmentVariable) string { - sortedSecrets := SortSecretsByKeys(secrets) - content := []byte{} - - for _, secret := range sortedSecrets { - content = append(content, []byte(secret.Key)...) - content = append(content, []byte(secret.Value)...) - } - - hash := sha256.Sum256(content) - return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:])) -} diff --git a/company/handbook/onboarding.mdx b/company/handbook/onboarding.mdx index e85be9f04..bcf47a339 100644 --- a/company/handbook/onboarding.mdx +++ b/company/handbook/onboarding.mdx @@ -19,10 +19,11 @@ Every new joiner has an onboarding buddy who should ideally be in the the same t 1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT. 2. Ship something together on day one – even if tiny! It feels great to hit the ground running, with a development environment all ready to go. 3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into. -4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1oy_NP1Q_Zt1oqxLpyNkLIGmhAI3N28AmZq6dDIOONSQ/edit?usp=sharing). +4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1RaJd3RoS2QpWLFHlgfHaXnHqCCwRt6mCGZkbJ75J_D0/edit?usp=sharing). 5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts. 6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better. 7. Change your Slack username in the users channel to `[NAME] (Infisical)`. 8. Go through the [technical overview](https://infisical.com/docs/internals/overview) of Infisical. +9. Request a company credit card (Maidul will be able to help with that). diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index bfa4bd693..74aa84947 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -47,20 +47,20 @@ $ infisical run -- npm run dev Used to fetch secrets via a [machine identity](/documentation/platform/identities/machine-identities) apposed to logged in credentials. Simply, export this variable in the terminal before running this command. ```bash - # Example - export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages. + # Example + export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id= --client-secret= --silent --plain) # --plain flag will output only the token, so it can be fed to an environment variable. --silent will disable any update messages. ``` Alternatively, you may use service tokens. Please note, however, that service tokens are being deprecated in favor of [machine identities](/documentation/platform/identities/machine-identities). They will be removed in the future in accordance with the deprecation notice and timeline stated [here](https://infisical.com/blog/deprecating-api-keys). - ```bash - # Example - export INFISICAL_TOKEN= + # Example + export INFISICAL_TOKEN= ``` - + + @@ -69,30 +69,22 @@ $ infisical run -- npm run dev To use, simply export this variable in the terminal before running this command. ```bash - # Example - export INFISICAL_DISABLE_UPDATE_CHECK=true + # Example + export INFISICAL_DISABLE_UPDATE_CHECK=true ``` + ### Flags - - By passing the `watch` flag, you are telling the CLI to watch for changes that happen in your Infisical project. - If secret changes happen, the command you provided will automatically be restarted with the new environment variables attached. - - ```bash - # Example - infisical run --watch -- printenv - ``` - - - + Explicitly set the directory where the .infisical.json resides. This is useful for some monorepo setups. ```bash - # Example - infisical run --project-config-dir=/some-dir -- printenv + # Example + infisical run --project-config-dir=/some-dir -- printenv ``` + @@ -180,19 +172,3 @@ $ infisical run -- npm run dev - - -## Automatically reload command when secrets change - -To automatically reload your command when secrets change, use the `--watch` flag. - -```bash -infisical run --watch -- npm run dev -``` - -This will watch for changes in your secrets and automatically restart your command with the new secrets. -When your command restarts, it will have the new environment variables injeceted into it. - - - Please note that this feature is intended for development purposes. It is not recommended to use this in production environments. Generally it's not recommended to automatically reload your application in production when remote changes are made. - \ No newline at end of file diff --git a/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx new file mode 100644 index 000000000..d02f47858 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/aws-elasticache.mdx @@ -0,0 +1,144 @@ +--- +title: "AWS Elasticahe" +description: "Learn how to dynamically generate Redis Database user credentials." +--- + +The Infisical Redis dynamic secret allows you to generate Redis Database credentials on demand based on configured role. + +## Prerequisites + + + +2. Create an AWS IAM user with the following permissions: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Action": [ + "elasticache:DescribeUsers", + "elasticache:ModifyUser", + "elasticache:CreateUser", + "elasticache:CreateUserGroup", + "elasticache:DeleteUser", + "elasticache:DescribeReplicationGroups", + "elasticache:DescribeUserGroups", + "elasticache:ModifyReplicationGroup", + "elasticache:ModifyUserGroup" + ], + "Resource": "arn:aws:elasticache:::user:*" + } + ] +} +``` + +3. Create an access key ID and secret access key for the user you created in the previous step. You will need these to configure the Infisical dynamic secret. + + + New leases may take up-to a couple of minutes before ElastiCache has the chance to complete their configuration. + It is recommended to use a retry strategy when establishing new Redis ElastiCache connections. + This may prevent errors when trying to use a password that isn't yet live on the targeted ElastiCache cluster. + + While a leasing is being created, you will be unable to create new leases for the same dynamic secret. + + + + Please ensure that your ElastiCache cluster has transit encryption enabled and set to required. This is required for the dynamic secret to work. + + + + + +## Set up Dynamic Secrets with Redis + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret. + + + + The region that the ElastiCache cluster is located in. _(e.g. us-east-1)_ + + + + This is the access key ID of the AWS IAM user you created in the prerequisites. This will be used to provision and manage the dynamic secret leases. + + + + This is the secret access key of the AWS IAM user you created in the prerequisites. This will be used to provision and manage the dynamic secret leases. + + + + A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. + + + + + If you want to provide specific privileges for the generated dynamic credentials, you can modify the ElastiCache statement to your needs. This is useful if you want to only give access to a specific table(s). + + ![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png) + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + If this step fails, you may have to add the CA certificate. + + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png) + + + +## Audit or Revoke Leases +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the expiration time of the lease or delete a lease before it's set time to live. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png) + +## Renew Leases +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + \ No newline at end of file diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 6ec5b48b9..7a3976e0f 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -1,6 +1,6 @@ --- title: "AWS IAM" -description: "How to dynamically generate AWS IAM Users." +description: "Learn how to dynamically generate AWS IAM Users." --- The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users on demand based on configured AWS policy. diff --git a/docs/documentation/platform/dynamic-secrets/cassandra.mdx b/docs/documentation/platform/dynamic-secrets/cassandra.mdx index 78e03e011..fd46c8288 100644 --- a/docs/documentation/platform/dynamic-secrets/cassandra.mdx +++ b/docs/documentation/platform/dynamic-secrets/cassandra.mdx @@ -1,6 +1,6 @@ --- title: "Cassandra" -description: "How to dynamically generate Cassandra database users." +description: "Learn how to dynamically generate Cassandra database user credentials" --- The Infisical Cassandra dynamic secret allows you to generate Cassandra database credentials on demand based on configured role. diff --git a/docs/documentation/platform/dynamic-secrets/mssql.mdx b/docs/documentation/platform/dynamic-secrets/mssql.mdx index 8dae71399..fb666adca 100644 --- a/docs/documentation/platform/dynamic-secrets/mssql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mssql.mdx @@ -1,6 +1,6 @@ --- title: "MS SQL" -description: "How to dynamically generate MS SQL database users." +description: "Learn how to dynamically generate MS SQL database user credentials." --- The Infisical MS SQL dynamic secret allows you to generate Microsoft SQL server database credentials on demand based on configured role. diff --git a/docs/documentation/platform/dynamic-secrets/mysql.mdx b/docs/documentation/platform/dynamic-secrets/mysql.mdx index c64edab63..d85f4b7bb 100644 --- a/docs/documentation/platform/dynamic-secrets/mysql.mdx +++ b/docs/documentation/platform/dynamic-secrets/mysql.mdx @@ -1,6 +1,6 @@ --- title: "MySQL" -description: "Learn how to dynamically generate MySQL Database user passwords." +description: "Learn how to dynamically generate MySQL Database user credentials." --- The Infisical MySQL dynamic secret allows you to generate MySQL Database credentials on demand based on configured role. diff --git a/docs/documentation/platform/dynamic-secrets/oracle.mdx b/docs/documentation/platform/dynamic-secrets/oracle.mdx index 05b832c4f..a6fb68913 100644 --- a/docs/documentation/platform/dynamic-secrets/oracle.mdx +++ b/docs/documentation/platform/dynamic-secrets/oracle.mdx @@ -1,6 +1,6 @@ --- title: "Oracle" -description: "Learn how to dynamically generate Oracle Database user passwords." +description: "Learn how to dynamically generate Oracle Database user credentials." --- The Infisical Oracle dynamic secret allows you to generate Oracle Database credentials on demand based on configured role. diff --git a/docs/documentation/platform/dynamic-secrets/overview.mdx b/docs/documentation/platform/dynamic-secrets/overview.mdx index 81ab42656..24c7fae4e 100644 --- a/docs/documentation/platform/dynamic-secrets/overview.mdx +++ b/docs/documentation/platform/dynamic-secrets/overview.mdx @@ -32,4 +32,5 @@ Dynamic secrets are particularly useful in environments with stringent security 2. [MySQL](./mysql) 3. [Cassandra](./cassandra) 4. [Oracle](./oracle) +6. [Redis](./redis) 5. [AWS IAM](./aws-iam) diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx index 13adfc750..ebc19b011 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -1,6 +1,6 @@ --- title: "PostgreSQL" -description: "How to dynamically generate PostgreSQL database users." +description: "Learn how to dynamically generate PostgreSQL database users." --- The Infisical PostgreSQL dynamic secret allows you to generate PostgreSQL database credentials on demand based on configured role. diff --git a/docs/documentation/platform/dynamic-secrets/redis.mdx b/docs/documentation/platform/dynamic-secrets/redis.mdx new file mode 100644 index 000000000..1270e6959 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/redis.mdx @@ -0,0 +1,106 @@ +--- +title: "Redis" +description: "Learn how to dynamically generate Redis Database user credentials." +--- + +The Infisical Redis dynamic secret allows you to generate Redis Database credentials on demand based on configured role. + +## Prerequisite +Create a user with the required permission in your Redis instance. This user will be used to create new accounts on-demand. + + +## Set up Dynamic Secrets with Redis + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-redis.png) + + + + Name by which you want the secret to be referenced + + + + Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate) + + + + Maximum time-to-live for a generated secret. + + + + The database host, this can be an IP address or a domain name as long as Infisical can reach it. + + + + The database port, this is the port that the Redis instance is listening on. + + + + Redis username that will be used to create new users on-demand. This is often 'default' or 'admin'. + + + + Password that will be used to create dynamic secrets. This is required if your Redis instance is password protected. + + + + A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service. + + + + + If you want to provide specific privileges for the generated dynamic credentials, you can modify the Redis statement to your needs. This is useful if you want to only give access to a specific table(s). + + ![Modify Redis Statements Modal](/images/platform/dynamic-secrets/modify-redis-statement.png) + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + If this step fails, you may have to add the CA certificate. + + + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png) + + + Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret. + + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png) + + + +## Audit or Revoke Leases +Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard. +This will allow you see the expiration time of the lease or delete a lease before it's set time to live. + +![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png) + +## Renew Leases +To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below. +![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png) + + + Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret + \ No newline at end of file diff --git a/docs/documentation/platform/pki/est.mdx b/docs/documentation/platform/pki/est.mdx index aab822dd5..ee66c32b6 100644 --- a/docs/documentation/platform/pki/est.mdx +++ b/docs/documentation/platform/pki/est.mdx @@ -21,7 +21,8 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g. - You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca). - The client devices need to have a bootstrap/pre-installed certificate. -- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS. +- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. + - For Infisical Cloud users, the devices must be configured to trust the [Amazon root CA certificates](https://www.amazontrust.com/repository). ## Guide to configuring EST @@ -42,7 +43,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g. 4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical. ![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png) - For demonstration, the complete URL of the supported EST endpoints will look like the following: + The complete URL of the supported EST endpoints will look like the following: - https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts - https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll diff --git a/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png new file mode 100644 index 000000000..537f20e73 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png new file mode 100644 index 000000000..db7f8be35 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png new file mode 100644 index 000000000..a7842b298 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png new file mode 100644 index 000000000..a2fd14d41 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache.png new file mode 100644 index 000000000..bf461c6a6 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-modal-redis.png b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-redis.png new file mode 100644 index 000000000..a4835e517 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-modal-redis.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-data-redis.png b/docs/images/platform/dynamic-secrets/lease-data-redis.png new file mode 100644 index 000000000..f3fd51637 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/lease-data-redis.png differ diff --git a/docs/images/platform/dynamic-secrets/lease-values-redis.png b/docs/images/platform/dynamic-secrets/lease-values-redis.png new file mode 100644 index 000000000..95d4a4ffd Binary files /dev/null and b/docs/images/platform/dynamic-secrets/lease-values-redis.png differ diff --git a/docs/images/platform/dynamic-secrets/modify-elasticache-statement.png b/docs/images/platform/dynamic-secrets/modify-elasticache-statement.png new file mode 100644 index 000000000..c8cd662d0 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/modify-elasticache-statement.png differ diff --git a/docs/images/platform/dynamic-secrets/modify-redis-statement.png b/docs/images/platform/dynamic-secrets/modify-redis-statement.png new file mode 100644 index 000000000..c9726f752 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/modify-redis-statement.png differ diff --git a/docs/images/platform/dynamic-secrets/provision-lease-redis.png b/docs/images/platform/dynamic-secrets/provision-lease-redis.png new file mode 100644 index 000000000..f5237d058 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/provision-lease-redis.png differ diff --git a/docs/integrations/platforms/ecs-with-agent.mdx b/docs/integrations/platforms/ecs-with-agent.mdx index 43760d2bb..31c5a982b 100644 --- a/docs/integrations/platforms/ecs-with-agent.mdx +++ b/docs/integrations/platforms/ecs-with-agent.mdx @@ -1,27 +1,30 @@ --- -title: 'Amazon ECS' +title: "Amazon ECS" description: "Learn how to deliver secrets to Amazon Elastic Container Service." --- ![ecs diagram](/images/guides/agent-with-ecs/ecs-diagram.png) -This guide will go over the steps needed to access secrets stored in Infisical from Amazon Elastic Container Service (ECS). +This guide will go over the steps needed to access secrets stored in Infisical from Amazon Elastic Container Service (ECS). -At a high level, the steps involve setting up an ECS task with a [Infisical Agent](/infisical-agent/overview) as a sidecar container. This sidecar container uses [Universal Auth](/documentation/platform/identities/universal-auth) to authenticate with Infisical to fetch secrets/access tokens. +At a high level, the steps involve setting up an ECS task with an [Infisical Agent](/infisical-agent/overview) as a sidecar container. This sidecar container uses [AWS Auth](/documentation/platform/identities/aws-auth) to authenticate with Infisical to fetch secrets/access tokens. Once the secrets/access tokens are retrieved, they are then stored in a shared [Amazon Elastic File System](https://aws.amazon.com/efs/) (EFS) volume. This volume is then made accessible to your application and all of its replicas. This guide primarily focuses on integrating Infisical Cloud with Amazon ECS on AWS Fargate and Amazon EFS. However, the principles and steps can be adapted for use with any instance of Infisical (on premise or cloud) and different ECS launch configurations. ## Prerequisites + This guide requires the following prerequisites: -- Infisical account + +- Infisical account - Git installed - Terraform v1.0 or later installed - Access to AWS credentials - Understanding of [Infisical Agent](/infisical-agent/overview) ## What we will deploy + For this demonstration, we'll deploy the [File Browser](https://github.com/filebrowser/filebrowser) application on our ECS cluster. Although this guide focuses on File Browser, the principles outlined here can be applied to any application of your choice. @@ -29,20 +32,20 @@ File Browser plays a key role in this context because it enables us to view all This feature is important for our demonstration, as it allows us to verify whether the Infisical agent is depositing the expected files into the designated file volume and if those files are accessible to the application. -Volumes that contain sensitive secrets should not be publicly accessible. The use of File Browser here is solely for demonstration and verification purposes. + Volumes that contain sensitive secrets should not be publicly accessible. The + use of File Browser here is solely for demonstration and verification + purposes. - ## Configure Authentication with Infisical -In order for the Infisical agent to fetch credentials from Infisical, we'll first need to authenticate with Infisical. -While Infisical supports various authentication methods, this guide focuses on using Universal Auth to authenticate the agent with Infisical. -Follow the documentation to configure and generate a client id and client secret with Universal auth [here](/documentation/platform/identities/universal-auth). -Make sure to save these credentials somewhere handy because you'll need them soon. +In order for the Infisical agent to fetch credentials from Infisical, we'll first need to authenticate with Infisical. Follow the documentation to configure a machine identity with AWS Auth [here](/documentation/platform/identities/aws-auth). +Take note of the Machine Identity ID as you will be needing this in the preceding steps. ## Clone guide assets repository + To help you quickly deploy the example application, please clone the guide assets from this [Github repository](https://github.com/Infisical/infisical-guides.git). -This repository contains assets for all Infisical guides. The content for this guide can be found within a sub directory called `aws-ecs-with-agent`. +This repository contains assets for all Infisical guides. The content for this guide can be found within a sub directory called `aws-ecs-with-agent`. The guide will assume that `aws-ecs-with-agent` is your working directory going forward. ## Deploy example application @@ -50,95 +53,84 @@ The guide will assume that `aws-ecs-with-agent` is your working directory going Before we can deploy our full application and its related infrastructure with Terraform, we'll need to first configure our Infisical agent. ### Agent configuration overview + The agent config file defines what authentication method will be used when connecting with Infisical along with where the fetched secrets/access tokens should be saved to. -Since the Infisical agent will be deployed as a sidecar, the agent configuration file and any secret template files will need to be encoded in base64. -This encoding step is necessary as it allows these files to be added into our Terraform configuration file without needing to upload them first. - -#### Secret template file -The Infisical agent accepts one or more optional template files. If provided, the agent will fetch secrets using the set authentication method and format the fetched secrets according to the given template file. - -For demonstration purposes, we will create the following secret template file. -This template will transform our secrets from Infisical project with the ID `62fd92aa8b63973fee23dec7`, in the `dev` environment, and secrets located in the path `/`, into a `KEY=VALUE` format. - - - Remember to update the project id, environment slug and secret path to one that exists within your Infisical project - - -```secrets.template secrets.template -{{- with secret "62fd92aa8b63973fee23dec7" "dev" "/" }} -{{- range . }} -{{ .Key }}={{ .Value }} -{{- end }} -{{- end }} -``` - -Next, we need encode this template file in `base64` so it can be set in the agent configuration file. - -```bash -cat secrets.template | base64 -Cnt7LSB3aXRoIHNlY3JldCAiMWVkMjk2MWQtNDM5NS00MmNlLTlkNzQtYjk2ZGQwYmYzMDg0IiAiZGV2IiAiLyIgfX0Ke3stIHJhbmdlIC4gfX0Ke3sgLktleSB9fT17eyAuVmFsdWUgfX0Ke3stIGVuZCB9fQp7ey0gZW5kIH19 -``` +Since the Infisical agent will be deployed as a sidecar, the agent configuration file will need to be encoded in base64. +This encoding step is necessary as it allows the agent configuration file to be added into our Terraform configuration without needing to upload it first. #### Full agent configuration file -This agent config file will connect with Infisical Cloud using Universal Auth and deposit access tokens at path `/infisical-agent/access-token` and render secrets to file `/infisical-agent/secrets`. -You'll notice that instead of passing the path to the secret template file as we normally would, we set the base64 encoded template from the previous step under `base64-template-content` property. +Inside the `aws-ecs-with-agent` directory, you will find a sample `agent-config.yaml` file. This agent config file will connect with Infisical Cloud using AWS Auth and deposit access tokens at path `/infisical-agent/access-token` and render secrets to file `/infisical-agent/secrets`. ```yaml agent-config.yaml infisical: address: https://app.infisical.com exit-after-auth: true auth: - type: universal-auth - config: - remove_client_secret_on_read: false + type: aws-iam sinks: - type: file config: path: /infisical-agent/access-token templates: - - base64-template-content: Cnt7LSB3aXRoIHNlY3JldCAiMWVkMjk2MWQtNDM5NS00MmNlLTlkNzQtYjk2ZGQwYmYzMDg0IiAiZGV2IiAiLyIgfX0Ke3stIHJhbmdlIC4gfX0Ke3sgLktleSB9fT17eyAuVmFsdWUgfX0Ke3stIGVuZCB9fQp7ey0gZW5kIH19 + - template-content: | + {{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }} + {{- range . }} + {{ .Key }}={{ .Value }} + {{- end }} + {{- end }} destination-path: /infisical-agent/secrets ``` -Again, we'll need to encode the full configuration file in `base64` so it can be easily delivered via Terraform. +#### Secret template -```bash -cat agent-config.yaml | base64 -aW5maXNpY2FsOgogIGFkZHJlc3M6IGh0dHBzOi8vYXBwLmluZmlzaWNhbC5jb20KICBleGl0LWFmdGVyLWF1dGg6IHRydWUKYXV0aDoKICB0eXBlOiB1bml2ZXJzYWwtYXV0aAogIGNvbmZpZzoKICAgIHJlbW92ZV9jbGllbnRfc2VjcmV0X29uX3JlYWQ6IGZhbHNlCnNpbmtzOgogIC0gdHlwZTogZmlsZQogICAgY29uZmlnOgogICAgICBwYXRoOiAvaW5maXNpY2FsLWFnZW50L2FjY2Vzcy10b2tlbgp0ZW1wbGF0ZXM6CiAgLSBiYXNlNjQtdGVtcGxhdGUtY29udGVudDogQ250N0xTQjNhWFJvSUhObFkzSmxkQ0FpTVdWa01qazJNV1F0TkRNNU5TMDBNbU5sTFRsa056UXRZamsyWkdRd1ltWXpNRGcwSWlBaVpHVjJJaUFpTHlJZ2ZYMEtlM3N0SUhKaGJtZGxJQzRnZlgwS2Uzc2dMa3RsZVNCOWZUMTdleUF1Vm1Gc2RXVWdmWDBLZTNzdElHVnVaQ0I5ZlFwN2V5MGdaVzVrSUgxOQogICAgZGVzdGluYXRpb24tcGF0aDogL2luZmlzaWNhbC1hZ2VudC9zZWNyZXRzCg== -``` +The Infisical agent accepts one or more optional templates. If provided, the agent will fetch secrets using the set authentication method and format the fetched secrets according to the given template. +Typically, these templates are passed in to the agent configuration file via file reference using the `source-path` property but for simplicity we define them inline. -## Add auth credentials & agent config -With the base64 encoded agent config file and Universal Auth credentials in hand, it's time to assign them as values in our Terraform config file. +In the agent configuration above, the template defined will transform the secrets from Infisical project with the ID `202f04d7-e4cb-43d4-a292-e893712d61fc`, in the `dev` environment, and secrets located in the path `/`, into a `KEY=VALUE` format. -To configure these values, navigate to the `ecs.tf` file in your preferred code editor and assign values to `auth_client_id`, `auth_client_secret`, and `agent_config`. + + Remember to update the project id, environment slug and secret path to one + that exists within your Infisical project + + +## Configure app on terraform + +Navigate to the `ecs.tf` file in your preferred code editor. In the container_definitions section, assign the values to the `machine_identity_id` and `agent_config` properties. +The `agent_config` property expects the base64-encoded agent configuration file. In order to get this, we use the `base64encode` and `file` functions of HCL. ```hcl ecs.tf ...snip... -data "template_file" "cb_app" { - template = file("./templates/ecs/cb_app.json.tpl") - - vars = { - app_image = var.app_image - sidecar_image = var.sidecar_image - app_port = var.app_port - fargate_cpu = var.fargate_cpu - fargate_memory = var.fargate_memory - aws_region = var.aws_region - auth_client_id = "" - auth_client_secret = "" - agent_config = "" +resource "aws_ecs_task_definition" "app" { + family = "cb-app-task" + execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = 4096 + memory = 8192 + container_definitions = templatefile("./templates/ecs/cb_app.json.tpl", { + app_image = var.app_image + sidecar_image = var.sidecar_image + app_port = var.app_port + fargate_cpu = var.fargate_cpu + fargate_memory = var.fargate_memory + aws_region = var.aws_region + machine_identity_id = "5655f4f5-332b-45f9-af06-8f493edff36f" + agent_config = base64encode(file("../agent-config.yaml")) + }) + volume { + name = "infisical-efs" + efs_volume_configuration { + file_system_id = aws_efs_file_system.infisical_efs.id + root_directory = "/" + } } } ...snip... ``` - - To keep this guide simple, `auth_client_id`, `auth_client_secret` have been added directly into the ECS container definition. - However, in production, you should securely fetch these values from AWS Secrets Manager or AWS Parameter store and feed them directly to agent sidecar. - - After these values have been set, they will be passed to the Infisical agent during startup through environment variables, as configured in the `infisical-sidecar` container below. ```terraform templates/ecs/cb_app.json.tpl @@ -169,12 +161,8 @@ After these values have been set, they will be passed to the Infisical agent dur }, "environment": [ { - "name": "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID", - "value": "${auth_client_id}" - }, - { - "name": "INFISICAL_UNIVERSAL_CLIENT_SECRET", - "value": "${auth_client_secret}" + "name": "INFISICAL_MACHINE_IDENTITY_ID", + "value": "${machine_identity_id}" }, { "name": "INFISICAL_AGENT_CONFIG_BASE64", @@ -191,9 +179,9 @@ After these values have been set, they will be passed to the Infisical agent dur ] ``` -In the above container definition, you'll notice that that the Infisical agent has a `mountPoints` defined. -This mount point is referencing to the already configured EFS volume as shown below. -`containerPath` is set to `/infisical-agent` because that is that the folder we have instructed the agent to deposit the credentials to. +In the above container definition, you'll notice that that the Infisical agent has a `mountPoints` defined. +This mount point is referencing to the already configured EFS volume as shown below. +`containerPath` is set to `/infisical-agent` because that is that the folder we have instructed the agent to deposit the credentials to. ```hcl terraform/efs.tf resource "aws_efs_file_system" "infisical_efs" { @@ -211,8 +199,9 @@ resource "aws_efs_mount_target" "mount" { ``` ## Configure AWS credentials + Because we'll be deploying the example file browser application to AWS via Terraform, you will need to obtain a set of `AWS Access Key` and `Secret Key`. -Once you have generated these credentials, export them to your terminal. +Once you have generated these credentials, export them to your terminal. 1. Export the AWS Access Key ID: @@ -227,26 +216,31 @@ Once you have generated these credentials, export them to your terminal. ``` ## Deploy terraform configuration + With the agent's sidecar configuration complete, we can now deploy our changes to AWS via Terraform. 1. Change your directory to `terraform` -```sh + +```sh cd terraform ``` 2. Initialize Terraform + ``` -$ terraform init +$ terraform init ``` -3. Preview resources that will be created +3. Preview resources that will be created + ``` $ terraform plan ``` 4. Trigger resource creation + ```bash -$ terraform apply +$ terraform apply Do you want to perform these actions? Terraform will perform the actions described above. @@ -264,24 +258,24 @@ Outputs: alb_hostname = "cb-load-balancer-1675475779.us-east-1.elb.amazonaws.com:8080" ``` -Once the resources have been successfully deployed, Terrafrom will output the host address where the file browser application will be accessible. -It may take a few minutes for the application to become fully ready. - +Once the resources have been successfully deployed, Terraform will output the host address where the file browser application will be accessible. +It may take a few minutes for the application to become fully ready. ## Verify secrets/tokens in EFS volume + To verify that the agent is depositing access tokens and rendering secrets to the paths specified in the agent config, navigate to the web address from the previous step. Once you visit the address, you'll be prompted to login. Enter the credentials shown below. ![file browser main login page](/images/guides/agent-with-ecs/file_browser_main.png) -Since our EFS volume is mounted to the path of the file browser application, we should see the access token and rendered secret file we defined via the agent config file. +Since our EFS volume is mounted to the path of the file browser application, we should see the access token and rendered secret file we defined via the agent config file. ![file browswer dashbaord](/images/guides/agent-with-ecs/filebrowser_afterlogin.png) -As expected, two files are present: `access-token` and `secrets`. -The `access-token` file should hold a valid `Bearer` token, which can be used to make HTTP requests to Infisical. +As expected, two files are present: `access-token` and `secrets`. +The `access-token` file should hold a valid `Bearer` token, which can be used to make HTTP requests to Infisical. The `secrets` file should contain secrets, formatted according to the specifications in our secret template file (presented in key=value format). ![file browser access token deposit](/images/guides/agent-with-ecs/access-token-deposit.png) -![file browser secrets render](/images/guides/agent-with-ecs/secrets-deposit.png) \ No newline at end of file +![file browser secrets render](/images/guides/agent-with-ecs/secrets-deposit.png) diff --git a/docs/integrations/platforms/infisical-agent.mdx b/docs/integrations/platforms/infisical-agent.mdx index 93c82f8fe..61e6f41ca 100644 --- a/docs/integrations/platforms/infisical-agent.mdx +++ b/docs/integrations/platforms/infisical-agent.mdx @@ -9,56 +9,60 @@ It eliminates the need to modify application logic by enabling clients to decide ![agent diagram](/images/agent/infisical-agent-diagram.png) ### Key features: + - Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume - Templating: Renders secrets via user provided templates to desired formats for applications to consume ### Token renewal + The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates. -When the Infisical Agent is started, it will attempt to obtain a valid access token using the authentication method you have configured. If the agent is unable to fetch a valid token, the agent will keep trying, increasing the time between each attempt. +When the Infisical Agent is started, it will attempt to obtain a valid access token using the authentication method you have configured. If the agent is unable to fetch a valid token, the agent will keep trying, increasing the time between each attempt. Once a access token is successfully fetched, the agent will make sure the access token stays valid, continuing to renew it before it expires. Every time the agent successfully retrieves a new access token, it writes the new token to the Sinks you've configured. - Access tokens can be utilized with Infisical SDKs or directly in API requests to retrieve secrets from Infisical + Access tokens can be utilized with Infisical SDKs or directly in API requests + to retrieve secrets from Infisical ### Templating -The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path. -Templating process is done through the use of Go language's [text/template feature](https://pkg.go.dev/text/template). Multiple template definitions can be set in the agent configuration file to generate a variety of formatted secret files. +The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path. -When the agent is started and templates are defined in the agent configuration file, the agent will attempt to acquire a valid access token using the set authentication method outlined in the agent's configuration. +Templating process is done through the use of Go language's [text/template feature](https://pkg.go.dev/text/template).You can refer to the available secret template functions [here](#available-secret-template-functions). Multiple template definitions can be set in the agent configuration file to generate a variety of formatted secret files. + +When the agent is started and templates are defined in the agent configuration file, the agent will attempt to acquire a valid access token using the set authentication method outlined in the agent's configuration. If this initial attempt is unsuccessful, the agent will momentarily pauses before continuing to make more attempts. -Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it. +Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it. It then formats these secrets using the user provided templates and writes the formatted data to configured file paths. -## Agent configuration file +## Agent configuration file -To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below. +To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below. While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional. -| Field | Description | -| ------------------------------------------------| ----------------------------- | -| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | -| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam`| -| `auth.config.identity-id` | The file path where the machine identity id is stored

This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. | -| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)

Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` | -| `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.

Please note that the file should be in JSON format. | -| `auth.config.client-id` | The file path where the universal-auth client id is stored. | -| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | -| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | -| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | -| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | -| `templates[].source-path` | The path to the template file that should be used to render secrets. | -| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | -| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) | -| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | -| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | - +| Field | Description | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | +| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` | +| `auth.config.identity-id` | The file path where the machine identity id is stored

This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. | +| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)

Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` | +| `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.

Please note that the file should be in JSON format. | +| `auth.config.client-id` | The file path where the universal-auth client id is stored. | +| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | +| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | +| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | +| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | +| `templates[].source-path` | The path to the template file that should be used to render secrets. | +| `templates[].template-content` | The inline secret template to be used for rendering the secrets. | +| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | +| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) | +| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | +| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) | ## Authentication @@ -68,19 +72,20 @@ The Infisical agent supports multiple authentication methods. Below are the avai The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. - - - - Path to the file containing the universal auth client ID. - - - Path to the file containing the universal auth client secret. - - - Instructs the agent to remove the client secret from disk after reading it. - - - + + + + Path to the file containing the universal auth client ID. + + + Path to the file containing the universal auth client secret. + + + Instructs the agent to remove the client secret from disk after reading + it. + + + @@ -98,21 +103,25 @@ The Infisical agent supports multiple authentication methods. Below are the avai remove_client_secret_on_read: false # Optional field, instructs the agent to remove the client secret from disk after reading it ``` + The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. - - - - Path to the file containing the machine identity ID. - - - Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`. - - - +{" "} + + + + + Path to the file containing the machine identity ID. + + + Path to the Kubernetes service account token to use. Default: + `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + + @@ -129,6 +138,7 @@ The Infisical agent supports multiple authentication methods. Below are the avai service-account-token: "/var/run/secrets/kubernetes.io/serviceaccount/token" # Optional field, custom path to the Kubernetes service account token to use ``` + @@ -186,6 +196,7 @@ The Infisical agent supports multiple authentication methods. Below are the avai ``` + The GCP IAM method is used to authenticate with Infisical with a GCP service account key. @@ -217,6 +228,7 @@ The Infisical agent supports multiple authentication methods. Below are the avai ``` + The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. @@ -244,10 +256,12 @@ The Infisical agent supports multiple authentication methods. Below are the avai ``` + ## Quick start Infisical Agent + To install the Infisical agent, you must first install the [Infisical CLI](../cli/overview) in the desired environment where you'd like the agent to run. This is because the Infisical agent is a sub-command of the Infisical CLI. Once you have the CLI installed, you will need to provision programmatic access for the agent via [Universal Auth](/documentation/platform/identities/universal-auth). To obtain a **Client ID** and a **Client Secret**, follow the step by step guide outlined [here](/documentation/platform/identities/universal-auth). @@ -277,8 +291,8 @@ templates: command: ./reload-app.sh ``` -The secret template below will be used to render the secrets with the key and the value separated by `=` sign. You'll notice that a custom function named `secret` is used to fetch the secrets. -This function takes the following arguments: `secret "" "" ""`. +The secret template below will be used to render the secrets with the key and the value separated by `=` sign. You'll notice that a custom function named `secret` is used to fetch the secrets. +This function takes the following arguments: `secret "" "" ""`. ```text my-dot-ev-secret-template {{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" }} @@ -290,13 +304,12 @@ This function takes the following arguments: `secret "" " ```bash listSecrets "" "environment-slug" "" @@ -309,11 +322,12 @@ infisical agent --config example-agent-config-file.yaml {{- end }} ``` - **Function name**: listSecrets +**Function name**: listSecrets - **Description**: This function can be used to render the full list of secrets within a given project, environment and secret path. +**Description**: This function can be used to render the full list of secrets within a given project, environment and secret path. + +**Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` - **Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` @@ -321,18 +335,18 @@ infisical agent --config example-agent-config-file.yaml getSecretByName "" "" "" "" ``` - ```bash example-template-usage - {{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} - {{ if .Value }} - password = "{{ .Value }}" - {{ end }} - {{ end }} - ``` +```bash example-template-usage +{{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} +{{ if .Value }} +password = "{{ .Value }}" +{{ end }} +{{ end }} +``` - **Function name**: getSecretByName +**Function name**: getSecretByName - **Description**: This function can be used to render a single secret by it's name. +**Description**: This function can be used to render a single secret by it's name. - **Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` +**Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` diff --git a/docs/mint.json b/docs/mint.json index 75368e3a8..34cb085a8 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -154,6 +154,8 @@ "documentation/platform/dynamic-secrets/mssql", "documentation/platform/dynamic-secrets/oracle", "documentation/platform/dynamic-secrets/cassandra", + "documentation/platform/dynamic-secrets/redis", + "documentation/platform/dynamic-secrets/aws-elasticache", "documentation/platform/dynamic-secrets/aws-iam" ] }, diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index b987e94cc..ff881e506 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -18,7 +18,9 @@ export type TDynamicSecret = { export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", - AwsIam = "aws-iam" + AwsIam = "aws-iam", + Redis = "redis", + AwsElastiCache = "aws-elasticache" } export enum SqlProviders { @@ -30,47 +32,72 @@ export enum SqlProviders { export type TDynamicSecretProvider = | { - type: DynamicSecretProviders.SqlDatabase; - inputs: { - client: SqlProviders; - host: string; - port: number; - database: string; - username: string; - password: string; - creationStatement: string; - revocationStatement: string; - renewStatement?: string; - ca?: string | undefined; - }; - } + type: DynamicSecretProviders.SqlDatabase; + inputs: { + client: SqlProviders; + host: string; + port: number; + database: string; + username: string; + password: string; + creationStatement: string; + revocationStatement: string; + renewStatement?: string; + ca?: string | undefined; + }; + } | { - type: DynamicSecretProviders.Cassandra; - inputs: { - host: string; - port: number; - keyspace?: string; - localDataCenter: string; - username: string; - password: string; - creationStatement: string; - revocationStatement: string; - renewStatement?: string; - ca?: string | undefined; - }; - } + type: DynamicSecretProviders.Cassandra; + inputs: { + host: string; + port: number; + keyspace?: string; + localDataCenter: string; + username: string; + password: string; + creationStatement: string; + revocationStatement: string; + renewStatement?: string; + ca?: string | undefined; + }; + } | { - type: DynamicSecretProviders.AwsIam; - inputs: { - accessKey: string; - secretAccessKey: string; - region: string; - awsPath?: string; - policyDocument?: string; - userGroups?: string; - policyArns?: string; + type: DynamicSecretProviders.AwsIam; + inputs: { + accessKey: string; + secretAccessKey: string; + region: string; + awsPath?: string; + policyDocument?: string; + userGroups?: string; + policyArns?: string; + }; + } + | { + type: DynamicSecretProviders.Redis; + inputs: { + host: string; + port: number; + username: string; + password?: string; + creationStatement: string; + renewStatement?: string; + revocationStatement: string; + ca?: string | undefined; + }; + } + | { + type: DynamicSecretProviders.AwsElastiCache; + inputs: { + clusterName: string; + accessKeyId: string; + secretAccessKey: string; + region: string; + creationStatement: string; + revocationStatement: string; + ca?: string | undefined; + }; }; - }; export type TCreateDynamicSecretDTO = { projectSlug: string; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 91901c8bc..6ee571d93 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -41,4 +41,5 @@ export type SubscriptionPlan = { caCrl: boolean; instanceUserManagement: boolean; externalKms: boolean; + pkiEst: boolean; }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 3724fc014..44bb2d8c9 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -210,6 +210,7 @@ export const IdentityKubernetesAuthForm = ({ label="Kubernetes Host / Base Kubernetes API URL " isError={Boolean(error)} errorText={error?.message} + tooltipText="The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running 'kubectl cluster-info'" isRequired > @@ -224,6 +225,7 @@ export const IdentityKubernetesAuthForm = ({ label="Token Reviewer JWT" isError={Boolean(error)} errorText={error?.message} + tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods." isRequired > @@ -237,6 +239,7 @@ export const IdentityKubernetesAuthForm = ({ @@ -252,6 +255,7 @@ export const IdentityKubernetesAuthForm = ({ label="Allowed Namespaces" isError={Boolean(error)} errorText={error?.message} + tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any namespaces." > @@ -262,7 +266,11 @@ export const IdentityKubernetesAuthForm = ({ defaultValue="" name="allowedAudience" render={({ field, fieldState: { error } }) => ( - + )} @@ -271,7 +279,11 @@ export const IdentityKubernetesAuthForm = ({ control={control} name="caCert" render={({ field, fieldState: { error } }) => ( - +