Merge branch 'main' into feat/pg-dynamic-secret

This commit is contained in:
Maidul Islam
2024-03-26 09:30:30 -04:00
committed by GitHub
56 changed files with 1632 additions and 469 deletions
+3 -3
View File
@@ -3,9 +3,6 @@
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION # THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# Required
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION # THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
@@ -16,6 +13,9 @@ POSTGRES_PASSWORD=infisical
POSTGRES_USER=infisical POSTGRES_USER=infisical
POSTGRES_DB=infisical POSTGRES_DB=infisical
# Required
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
# Redis # Redis
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379
@@ -0,0 +1,149 @@
name: Deployment pipeline
on: [workflow_dispatch]
permissions:
id-token: write
contents: read
jobs:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
- name: 🏗️ Build backend and push to docker hub
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }}
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [infisical-image]
environment:
name: Gamma
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: us-east-1
role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-prod-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-prod-platform
cluster: infisical-prod-platform
wait-for-service-stability: true
production-postgres-deployment:
name: Deploy to production
runs-on: ubuntu-latest
needs: [gamma-deployment]
environment:
name: Production
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
aws-region: us-east-1
role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-prod-platform
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-prod-platform
cluster: infisical-prod-platform
wait-for-service-stability: true
@@ -1,122 +0,0 @@
name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
jobs:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_FOR_ECR }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_FOR_ECR }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
- name: 🏗️ Build backend and push to docker hub
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
postgres-migration:
name: Run latest migration files
runs-on: ubuntu-latest
needs: [infisical-image]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Change directory to backend and install dependencies
env:
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
run: |
cd backend
npm install
npm run migration:latest
# - name: Run postgres DB migration files
# env:
# DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
# run: npm run migration:latest
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [postgres-migration]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: v3.10.0
- name: Install infisical helm chart
run: |
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
- name: Install kubectl
uses: azure/setup-kubectl@v3
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 infisical-gamma-postgres
- name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma
- name: test kubectl
run: kubectl get ingress
- name: Download helm values to file and upgrade gamma deploy
run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1
else
echo "Helm upgrade was successful"
fi
+32
View File
@@ -194,6 +194,25 @@ export const FOLDERS = {
} }
} as const; } as const;
export const SECRETS = {
ATTACH_TAGS: {
secretName: "The name of the secret to attach tags to.",
secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
tagSlugs: "An array of tag slugs to attach to the secret."
},
DETACH_TAGS: {
secretName: "The name of the secret to detach tags from.",
secretPath: "The path of the secret to detach tags from.",
type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located",
projectSlug: "The slug of the project where the secret is located",
tagSlugs: "An array of tag slugs to detach from the secret."
}
} as const;
export const RAW_SECRETS = { export const RAW_SECRETS = {
LIST: { LIST: {
workspaceId: "The ID of the project to list secrets from.", workspaceId: "The ID of the project to list secrets from.",
@@ -361,5 +380,18 @@ export const DYNAMIC_SECRET_LEASES = {
leaseId: "The ID of the dynamic secret lease.", leaseId: "The ID of the dynamic secret lease.",
isForced: isForced:
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally." "A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
export const SECRET_TAGS = {
LIST: {
projectId: "The ID of the project to list tags from."
},
CREATE: {
projectId: "The ID of the project to create the tag in.",
name: "The name of the tag to create.",
slug: "The slug of the tag to create.",
color: "The color of the tag to create."
},
DELETE: {
tagId: "The ID of the tag to delete.",
projectId: "The ID of the project to delete the tag from."
} }
} as const; } as const;
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas"; import { SecretTagsSchema } from "@app/db/schemas";
import { SECRET_TAGS } from "@app/lib/api-docs";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -10,7 +11,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
schema: { schema: {
params: z.object({ params: z.object({
projectId: z.string().trim() projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -36,12 +37,12 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
schema: { schema: {
params: z.object({ params: z.object({
projectId: z.string().trim() projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
name: z.string().trim(), name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
slug: z.string().trim(), slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
color: z.string() color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -68,8 +69,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
method: "DELETE", method: "DELETE",
schema: { schema: {
params: z.object({ params: z.object({
projectId: z.string().trim(), projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
tagId: z.string().trim() tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
}), }),
response: { response: {
200: z.object({ 200: z.object({
+119 -1
View File
@@ -10,7 +10,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { RAW_SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
@@ -23,6 +23,124 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas"; import { secretRawSchema } from "../sanitizedSchemas";
export const registerSecretRouter = async (server: FastifyZodProvider) => { export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/tags/:secretName",
method: "POST",
schema: {
description: "Attach tags to a secret",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(SECRETS.ATTACH_TAGS.secretName)
}),
body: z.object({
projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(SECRETS.ATTACH_TAGS.secretPath),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.ATTACH_TAGS.type),
tagSlugs: z.string().array().min(1).describe(SECRETS.ATTACH_TAGS.tagSlugs)
}),
response: {
200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secret = await server.services.secret.attachTags({
secretName: req.params.secretName,
tagSlugs: req.body.tagSlugs,
path: req.body.secretPath,
environment: req.body.environment,
type: req.body.type,
projectSlug: req.body.projectSlug,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { secret };
}
});
server.route({
url: "/tags/:secretName",
method: "DELETE",
schema: {
description: "Detach tags from a secret",
security: [
{
bearerAuth: []
}
],
params: z.object({
secretName: z.string().trim().describe(SECRETS.DETACH_TAGS.secretName)
}),
body: z.object({
projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug),
environment: z.string().trim().describe(SECRETS.DETACH_TAGS.environment),
secretPath: z
.string()
.trim()
.default("/")
.transform(removeTrailingSlash)
.describe(SECRETS.DETACH_TAGS.secretPath),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.DETACH_TAGS.type),
tagSlugs: z.string().array().min(1).describe(SECRETS.DETACH_TAGS.tagSlugs)
}),
response: {
200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({
tags: SecretTagsSchema.pick({
id: true,
slug: true,
name: true,
color: true
}).array()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secret = await server.services.secret.detachTags({
secretName: req.params.secretName,
tagSlugs: req.body.tagSlugs,
path: req.body.secretPath,
environment: req.body.environment,
type: req.body.type,
projectSlug: req.body.projectSlug,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { secret };
}
});
server.route({ server.route({
url: "/raw", url: "/raw",
method: "GET", method: "GET",
+5 -1
View File
@@ -168,8 +168,12 @@ export const projectDALFactory = (db: TDbClient) => {
} }
}; };
const findProjectBySlug = async (slug: string, orgId: string) => { const findProjectBySlug = async (slug: string, orgId: string | undefined) => {
try { try {
if (!orgId) {
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
}
const projects = await db(TableName.ProjectMembership) const projects = await db(TableName.ProjectMembership)
.where(`${TableName.Project}.slug`, slug) .where(`${TableName.Project}.slug`, slug)
.where(`${TableName.Project}.orgId`, orgId) .where(`${TableName.Project}.orgId`, orgId)
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -284,10 +284,11 @@ export const projectServiceFactory = ({
// Get the role permission for the identity // Get the role permission for the identity
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
ProjectMembershipRole.Admin, OrgMembershipRole.Member,
organization.id organization.id
); );
// Identity has to be at least a member in order to create projects
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPrivilege) if (!hasPrivilege)
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
+22
View File
@@ -150,6 +150,27 @@ export const secretDALFactory = (db: TDbClient) => {
} }
}; };
const getSecretTags = async (secretId: string, tx?: Knex) => {
try {
const tags = await (tx || db)(TableName.JnSecretTag)
.join(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
.where({ [`${TableName.Secret}Id` as const]: secretId })
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
return tags.map((el) => ({
id: el.tagId,
color: el.tagColor,
slug: el.tagSlug,
name: el.tagName
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret tags" });
}
};
const findByBlindIndexes = async ( const findByBlindIndexes = async (
folderId: string, folderId: string,
blindIndexes: Array<{ blindIndex: string; type: SecretType }>, blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
@@ -184,6 +205,7 @@ export const secretDALFactory = (db: TDbClient) => {
bulkUpdate, bulkUpdate,
deleteMany, deleteMany,
bulkUpdateNoVersionIncrement, bulkUpdateNoVersionIncrement,
getSecretTags,
findByFolderId, findByFolderId,
findByBlindIndexes findByBlindIndexes
}; };
+206 -1
View File
@@ -22,6 +22,7 @@ import { TSecretDALFactory } from "./secret-dal";
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue"; import { TSecretQueueFactory } from "./secret-queue";
import { import {
TAttachSecretTagsDTO,
TCreateBulkSecretDTO, TCreateBulkSecretDTO,
TCreateSecretDTO, TCreateSecretDTO,
TCreateSecretRawDTO, TCreateSecretRawDTO,
@@ -47,7 +48,7 @@ type TSecretServiceFactoryDep = {
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory; secretVersionDAL: TSecretVersionDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@@ -307,6 +308,7 @@ export const secretServiceFactory = ({
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
const { secretName, ...el } = inputSecret; const { secretName, ...el } = inputSecret;
const updatedSecret = await secretDAL.transaction(async (tx) => const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({ fnSecretBulkUpdate({
folderId, folderId,
@@ -442,6 +444,7 @@ export const secretServiceFactory = ({
const folderId = folder.id; const folderId = folder.id;
const secrets = await secretDAL.findByFolderId(folderId, actorId); const secrets = await secretDAL.findByFolderId(folderId, actorId);
if (includeImports) { if (includeImports) {
const secretImports = await secretImportDAL.find({ folderId }); const secretImports = await secretImportDAL.find({ folderId });
const allowedImports = secretImports.filter(({ importEnv, importPath }) => const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
@@ -994,7 +997,209 @@ export const secretServiceFactory = ({
return secretVersions; return secretVersions;
}; };
const attachTags = async ({
secretName,
tagSlugs,
path: secretPath,
environment,
type,
projectSlug,
actor,
actorAuthMethod,
actorOrgId,
actorId
}: TAttachSecretTagsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
await projectDAL.checkProjectUpgradeStatus(project.id);
const secret = await getSecretByName({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId: project.id,
environment,
path: secretPath,
secretName,
type
});
if (!secret) {
throw new BadRequestError({ message: "Secret not found" });
}
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
const tags = await secretTagDAL.find({
projectId: project.id,
$in: {
slug: tagSlugs
}
});
if (tags.length !== tagSlugs.length) {
throw new BadRequestError({ message: "One or more tags not found." });
}
const secretTags = await secretDAL.getSecretTags(secret.id);
if (secretTags.some((tag) => tagSlugs.includes(tag.slug))) {
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
}
const combinedTags = new Set([...secretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId: folder.id,
projectId: project.id,
inputSecrets: [
{
filter: { id: secret.id },
data: {
tags: Array.from(combinedTags)
}
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
return {
...updatedSecret[0],
tags: [...secretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
};
};
const detachTags = async ({
secretName,
tagSlugs,
path: secretPath,
environment,
type,
projectSlug,
actor,
actorAuthMethod,
actorOrgId,
actorId
}: TAttachSecretTagsDTO) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
await projectDAL.checkProjectUpgradeStatus(project.id);
const secret = await getSecretByName({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId: project.id,
environment,
path: secretPath,
secretName,
type
});
if (!secret) {
throw new BadRequestError({ message: "Secret not found" });
}
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
if (!folder) {
throw new BadRequestError({ message: "Folder not found" });
}
const tags = await secretTagDAL.find({
projectId: project.id,
$in: {
slug: tagSlugs
}
});
if (tags.length !== tagSlugs.length) {
throw new BadRequestError({ message: "One or more tags not found." });
}
const secretTags = await secretDAL.getSecretTags(secret.id);
// Make sure all the tags exist on the secret
const tagIdsToRemove = tags.map((tag) => tag.id);
const secretTagIds = secretTags.map((tag) => tag.id);
if (!tagIdsToRemove.every((el) => secretTagIds.includes(el))) {
throw new BadRequestError({ message: "One or more tags not found on the secret" });
}
const newTags = secretTags.filter((tag) => !tagIdsToRemove.includes(tag.id));
const updatedSecret = await secretDAL.transaction(async (tx) =>
fnSecretBulkUpdate({
folderId: folder.id,
projectId: project.id,
inputSecrets: [
{
filter: { id: secret.id },
data: {
tags: newTags.map((tag) => tag.id)
}
}
],
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
await snapshotService.performSnapshot(folder.id);
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
return {
...updatedSecret[0],
tags: newTags
};
};
return { return {
attachTags,
detachTags,
createSecret, createSecret,
deleteSecret, deleteSecret,
updateSecret, updateSecret,
@@ -206,6 +206,15 @@ export type TFnSecretBulkUpdate = {
tx?: Knex; tx?: Knex;
}; };
export type TAttachSecretTagsDTO = {
projectSlug: string;
secretName: string;
tagSlugs: string[];
environment: string;
path: string;
type: SecretType;
} & Omit<TProjectPermission, "projectId">;
export type TFnSecretBulkDelete = { export type TFnSecretBulkDelete = {
folderId: string; folderId: string;
projectId: string; projectId: string;
+2 -2
View File
@@ -406,14 +406,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
return nil return nil
} }
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error { func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
var secretsResponse GetEncryptedSecretsV3Response var secretsResponse GetEncryptedSecretsV3Response
response, err := httpClient. response, err := httpClient.
R(). R().
SetResult(&secretsResponse). SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT). SetHeader("User-Agent", USER_AGENT).
SetBody(request). SetBody(request).
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, secretName))
if err != nil { if err != nil {
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err) return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
-1
View File
@@ -401,7 +401,6 @@ type DeleteSecretV3Request struct {
} }
type UpdateSecretByNameV3Request struct { type UpdateSecretByNameV3Request struct {
SecretName string `json:"secretName"`
WorkspaceID string `json:"workspaceId"` WorkspaceID string `json:"workspaceId"`
Environment string `json:"environment"` Environment string `json:"environment"`
Type string `json:"type"` Type string `json:"type"`
+1 -2
View File
@@ -297,7 +297,6 @@ var secretsSetCmd = &cobra.Command{
updateSecretRequest := api.UpdateSecretByNameV3Request{ updateSecretRequest := api.UpdateSecretByNameV3Request{
WorkspaceID: workspaceFile.WorkspaceId, WorkspaceID: workspaceFile.WorkspaceId,
Environment: environmentName, Environment: environmentName,
SecretName: secret.PlainTextKey,
SecretValueCiphertext: secret.SecretValueCiphertext, SecretValueCiphertext: secret.SecretValueCiphertext,
SecretValueIV: secret.SecretValueIV, SecretValueIV: secret.SecretValueIV,
SecretValueTag: secret.SecretValueTag, SecretValueTag: secret.SecretValueTag,
@@ -305,7 +304,7 @@ var secretsSetCmd = &cobra.Command{
SecretPath: secretsPath, SecretPath: secretsPath,
} }
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest) err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
if err != nil { if err != nil {
util.HandleError(err, "Unable to process secret update request") util.HandleError(err, "Unable to process secret update request")
return return
@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/workspace/{projectId}/tags"
---
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/workspace/{projectId}/tags"
---
@@ -0,0 +1,4 @@
---
title: "Attach tags"
openapi: "POST /api/v3/secrets/tags/{secretName}"
---
@@ -0,0 +1,4 @@
---
title: "Detach tags"
openapi: "DELETE /api/v3/secrets/tags/{secretName}"
---
@@ -1,37 +1,102 @@
--- ---
title: "MySQL/MariaDB" title: "MySQL/MariaDB"
description: "Rotated database user password of a MySQL or MariaDB" description: "How to rotate MySQL/MariaDB database user passwords"
--- ---
Infisical will update periodically the provided database user's password. The Infisical MySQL secret rotation allows you to automatically rotate your MySQL database user's password at a predefined interval.
<Warning>
At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud.
</Warning>
## Working ## Prerequisite
1. User's has to create the two user's for Infisical to rotate and provide them required database access 1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`.
2. Infisical will connect with your database with admin access 2. Create another MySQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
3. If last rotated one was username1, then username2 is chosen to be rotated
5. Update it's password with random value To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html).
6. After testing it gets saved to the provided secret mapping
## How it works
1. Infisical connects to your database using the provided `admin` user account.
2. A random value is generated and the password for `user-a` is updated with the new value.
3. The new password is then tested by logging into the database
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
5. The process is then repeated for `user-b` on the next rotation.
6. The cycle repeats until secret rotation is deleted/stopped.
## Rotation Configuration ## Rotation Configuration
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` <Steps>
2. Click on `MySQL` <Step title="Open Secret Rotation Page">
3. Provide the inputs Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
- Admin Username: DB admin username </Step>
- Admin Password: DB admin password <Step title="Click on MySQL card" />
- Host: DB host <Step title="Provide the inputs">
- Port: DB port(number) <ParamField path="Admin Username" type="string" required>
- Username1: The first username in two to rotate Rotator admin username
- Username2: The second username in two to rotate </ParamField>
- CA: Certificate to connect with database(string)
4. Final step
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- Finally select the secrets in your provided board to replace with new secret after each rotation
- Your done and good to go.
Congrats. You have 10x your MySQL/MariaDB access security. <ParamField path="Admin password" type="string" required>
Rotator admin password
</ParamField>
<ParamField path="Host" type="string" required>
Database host url
</ParamField>
<ParamField path="Port" type="number" required>
Database port number
</ParamField>
<ParamField path="Username1" type="string" required>
The first username of two to rotate - `user-a`
</ParamField>
<ParamField path="Username2" type="string" required>
The second username of two to rotate - `user-b`
</ParamField>
<ParamField path="CA" type="string">
Optional database certificate to connect with database
</ParamField>
</Step>
<Step title="Configure the output secret mapping">
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
<ParamField path="Environment" type="string" required>
The environment where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Secret Path" type="string" required>
The secret path where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Interval" type="number" required>
What interval should the credentials be rotated in days.
</ParamField>
<ParamField path="DB USERNAME" type="string" required>
Select an existing secret key where the rotated database username value should be saved to.
</ParamField>
<ParamField path="DB PASSWORD" type="string" required>
Select an existing select key where the rotated database password value should be saved to.
</ParamField>
</Step>
</Steps>
## FAQ
<AccordionGroup>
<Accordion title="Why can't we delete the other user when rotating?">
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
</Accordion>
<Accordion title="Why do you need root user account?">
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
</Accordion>
</AccordionGroup>
@@ -1,33 +1,104 @@
--- ---
title: "PostgreSQL/CockroachDB" title: "PostgreSQL/CockroachDB"
description: "Rotated database user password of a PostgreSQL or Cockroach DB" description: "How to rotate postgreSQL/cockroach database user passwords"
--- ---
Infisical will update periodically the provided database user's password. The Infisical Postgres secret rotation allows you to automatically rotate your Postgres database user's password at a predefined interval.
## Working
1. User's has to create the two user's for Infisical to rotate and provide them required database access. ## Prerequisite
2. Infisical will connect with your database with admin access.
3. If last rotated one was username1, then username2 is chosen to be rotated. 1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`.
5. Update it's password with random value. 2. Create another PostgreSQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
6. After testing it gets saved to the provided secret mapping.
To learn more about Postgres permission system, please visit this [documentation](https://www.postgresql.org/docs/9.1/sql-grant.html).
## How it works
1. Infisical connects to your database using the provided `admin` user account.
2. A random value is generated and the password for `user-a` is updated with the new value.
3. The new password is then tested by logging into the database
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
5. The process is then repeated for `user-b` on the next rotation.
6. The cycle repeats until secret rotation is deleted/stopped.
## Rotation Configuration ## Rotation Configuration
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` <Steps>
2. Click on `PostgreSQL` <Step title="Open Secret Rotation Page">
3. Provide the inputs Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
- Admin Username: DB admin username </Step>
- Admin Password: DB admin password <Step title="Click on PostgresSQL card" />
- Host: DB host
- Port: DB port(number)
- Username1: The first username in two to rotate
- Username2: The second username in two to rotate
- CA: Certificate to connect with database(string)
4. Final step
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
- Finally select the secrets in your provided board to replace with new secret after each rotation
- Your done and good to go.
Congratulations. You have improved your PostgreSQL/CockroachDB access security. <Step title="Provide the inputs">
<ParamField path="Admin Username" type="string" required="true">
Rotator admin username
</ParamField>
<ParamField path="Admin password" type="string" required="true">
Rotator admin password
</ParamField>
<ParamField path="Host" type="string" required="true">
Database host url
</ParamField>
<ParamField path="Port" type="number" required="true">
Database port number
</ParamField>
<ParamField path="Username1" type="string" required="true">
The first username of two to rotate - `user-a`
</ParamField>
<ParamField path="Username2" type="string" required="true">
The second username of two to rotate - `user-b`
</ParamField>
<ParamField path="CA" type="string" optional>
Optional database certificate to connect with database
</ParamField>
</Step>
<Step title="Configure the output secret mapping">
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
<ParamField path="Environment" type="string" required>
The environment where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Secret Path" type="string" required>
The secret path where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Interval" type="number" required>
What interval should the credentials be rotated in days.
</ParamField>
<ParamField path="DB USERNAME" type="string" required>
Select an existing secret key where the rotated database username value should be saved to.
</ParamField>
<ParamField path="DB PASSWORD" type="string" required>
Select an existing select key where the rotated database password value should be saved to.
</ParamField>
</Step>
</Steps>
## FAQ
<AccordionGroup>
<Accordion title="Why can't we delete the other user when rotating?">
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
</Accordion>
<Accordion title="Why do you need root user account?">
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
</Accordion>
</AccordionGroup>
@@ -1,31 +1,58 @@
--- ---
title: "Twilio SendGrid" title: "Twilio SendGrid"
description: "Rotate Twilio SendGrid API keys" description: "How to rotate Twilio SendGrid API keys"
--- ---
Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails. Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical.
It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys.
## Working ## Prerequisite
1. Infisical will need an admin token of SendGrid to create API keys dynamically. You will need a valid SendGrid admin key with the necessary scope to create additional API keys.
2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically
3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) Follow the [SendGrid Docs to create an admin api key](https://docs.sendgrid.com/ui/account-and-settings/api-keys)
## How it works
Using the provided admin API key, Infisical will attempt to create child API keys with the specified permissions.
New keys will ge generated every time a rotation occurs. Behind the scenes, Infisical uses the [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) to generate new API keys.
## Rotation Configuration ## Rotation Configuration
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` <Steps>
2. Click on `Twilio SendGrid Card` <Step title="Open Secret Rotation Page">
3. Provide the inputs Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
- Admin API Key: </Step>
SendGrid admin key to create lower scoped API keys. <Step title="Click on Twilio SendGrid Card" />
- API Key Scopes <Step title="Provide the inputs">
SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions) <ParamField path="Admin API Key" type="string" required>
SendGrid admin API key with permission to create child scoped API keys.
</ParamField>
4. Final step <ParamField path="Admin API Key" type="array" required>
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets The permissions that the newly generated API keys will have. To view possible permissions, visit [this documentation](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions).
- Finally select the secrets in your provided board to replace with new secret after each rotation Permissions must be entered as a list of strings.
- Your done and good to go.
Now your output mapped secret value will be replaced periodically by SendGrid.
Example: `["user.profile.read", "user.profile.update"]`
</ParamField>
</Step>
<Step title="Configure the output secret mapping">
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
<ParamField path="Environment" type="string" required>
The environment where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Secret Path" type="string" required>
The secret path where the rotated credentials should be mapped to.
</ParamField>
<ParamField path="Interval" type="number" required>
What interval should the credentials be rotated in days.
</ParamField>
<ParamField path="API KEY" type="string" required>
Select an existing select key where the newly rotated API key will get saved to.
</ParamField>
</Step>
</Steps>
Now your output mapped secret value will be replaced periodically by SendGrid.
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+91 -35
View File
@@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme
## Install Operator ## Install Operator
The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
<Tabs> <Tabs>
<Tab title="Helm (recommended)"> <Tab title="Helm (recommended)">
@@ -61,23 +61,38 @@ Once you have installed the operator to your cluster, you'll need to create a `I
apiVersion: secrets.infisical.com/v1alpha1 apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret kind: InfisicalSecret
metadata: metadata:
# Name of of this InfisicalSecret resource name: infisicalsecret-sample
name: infisicalsecret-sample labels:
label-to-be-passed-to-managed-secret: sample-value
annotations:
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
spec: spec:
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used hostAPI: https://app.infisical.com/api
hostAPI: https://app.infisical.com/api resyncInterval: 10
resyncInterval: 60 authentication:
authentication: # Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
serviceToken: # If you have multiple authentication methods defined, it may cause issues.
serviceTokenSecretReference: universalAuth:
secretName: service-token secretsScope:
projectSlug: <project-slug>
envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: "<secrets-path>" # Root is "/"
credentialsRef:
secretName: universal-auth-credentials
secretNamespace: default
serviceToken:
serviceTokenSecretReference:
secretName: service-token
secretNamespace: default
secretsScope:
envSlug: <env-slug>
secretsPath: <secrets-path> # Root is "/"
managedSecretReference:
secretName: managed-secret
secretNamespace: default secretNamespace: default
secretsScope: # secretType: kubernetes.io/dockerconfigjson
envSlug: dev
secretsPath: "/"
managedSecretReference:
secretName: managed-secret # <-- the name of kubernetes secret that will be created
secretNamespace: default # <-- where the kubernetes secret should be created
``` ```
### InfisicalSecret CRD properties ### InfisicalSecret CRD properties
@@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute.
</Accordion> </Accordion>
<Accordion title="authentication"> <Accordion title="authentication">
This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched. Currently, only [Service Tokens](../../documentation/platform/token) can be used to authenticate with Infisical. This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched
</Accordion> </Accordion>
<Accordion title="authentication.serviceToken.serviceTokenSecretReference"> <Accordion title="authentication.universalAuth">
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and name space of secret that stores this service token. The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials.
<Steps>
<Step title="Create a machine identity">
You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth).
</Step>
<Step title="Create Kubernetes secret containing machine identity credentials">
Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials.
To quickly create a Kubernetes secret containing the identity credentials, you can run the command below.
Make sure you replace `<your-identity-client-id>` with the identity client ID and `<your-identity-client-secret>` with the identity client secret.
``` bash
kubectl create secret generic universal-auth-credentials --from-literal=clientId="<your-identity-client-id>" --from-literal=clientSecret="<your-identity-client-secret>"
```
</Step>
<Step title="Add reference for the Kubernetes secret containing the identity credentials">
Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource.
</Step>
</Steps>
<Info>
Make sure to also populate the `secretsScope` field with the project slug _`projectSlug`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
</Info>
## Example
```yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret
metadata:
name: infisicalsecret-sample-crd
spec:
authentication:
universalAuth:
secretsScope:
projectSlug: <project-slug> # <-- project slug
envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: "<secrets-path>" # Root is "/"
credentialsRef:
secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials
secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials
...
```
</Accordion>
<Accordion title="authentication.serviceToken">
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token.
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD. Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
#### 1. Generate service token #### 1. Generate service token
@@ -122,13 +186,17 @@ Default re-sync interval is every 1 minute.
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token. To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
``` bash ``` bash
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here> kubectl create secret generic service-token --from-literal=infisicalToken="<your-service-token-here>"
``` ```
#### 3. Add reference for the Kubernetes secret containing service token #### 3. Add reference for the Kubernetes secret containing service token
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource. Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
<Info>
Make sure to also populate the `secretsScope` field with the, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
</Info>
## Example ## Example
```yaml ```yaml
apiVersion: secrets.infisical.com/v1alpha1 apiVersion: secrets.infisical.com/v1alpha1
@@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute.
serviceTokenSecretReference: serviceTokenSecretReference:
secretName: service-token # <-- name of the Kubernetes secret that stores our service token secretName: service-token # <-- name of the Kubernetes secret that stores our service token
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
secretsScope:
envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: <secrets-path> # Root is "/"
... ...
``` ```
</Accordion> </Accordion>
<Accordion title="authentication.serviceToken.secretsScope">
This block defines the scope of what secrets should be fetched. This is needed as your service token can have access to multiple folders and environments.
A scope is defined by `envSlug` and `secretsPath`.
#### envSlug
This refers to the short hand name of an environment. For example for the `development` environment the environment slug is `dev`. You can locate the slug of your environment by heading to your project settings in the Infisical dashboard.
#### secretsPath
secretsPath is the path to the secret in the given environment. For example a path of `/` would refer to the root of the environment whereas `/folder1` would refer to the secrets in folder1 from the root.
Both fields are required.
</Accordion>
<Accordion title="managedSecretReference"> <Accordion title="managedSecretReference">
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project. The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets. This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
+11 -1
View File
@@ -467,6 +467,14 @@
"api-reference/endpoints/folders/delete" "api-reference/endpoints/folders/delete"
] ]
}, },
{
"group": "Secret tags",
"pages": [
"api-reference/endpoints/secret-tags/list",
"api-reference/endpoints/secret-tags/create",
"api-reference/endpoints/secret-tags/delete"
]
},
{ {
"group": "Secrets", "group": "Secrets",
"pages": [ "pages": [
@@ -474,7 +482,9 @@
"api-reference/endpoints/secrets/create", "api-reference/endpoints/secrets/create",
"api-reference/endpoints/secrets/read", "api-reference/endpoints/secrets/read",
"api-reference/endpoints/secrets/update", "api-reference/endpoints/secrets/update",
"api-reference/endpoints/secrets/delete" "api-reference/endpoints/secrets/delete",
"api-reference/endpoints/secrets/attach-tags",
"api-reference/endpoints/secrets/detach-tags"
] ]
}, },
{ {
@@ -21,13 +21,13 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
if (!isVisible) return replaceContentWithDot(content); if (!isVisible) return replaceContentWithDot(content);
let skipNext = false; let skipNext = false;
const formatedContent = content.split(REGEX).flatMap((el, i) => { const formattedContent = content.split(REGEX).flatMap((el, i) => {
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
if (isInterpolationSyntax) { if (isInterpolationSyntax) {
skipNext = true; skipNext = true;
return ( return (
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}> <span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
&#36;&#123;<span className="ph-no-capture text-yello-200/80">{el.slice(2, -1)}</span> &#36;&#123;<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#125; &#125;
</span> </span>
); );
@@ -41,7 +41,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough // akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
// when break is added a line break works properly // when break is added a line break works properly
return formatedContent.concat(<br />); return formattedContent.concat(<br />);
}; };
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & { type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
+2 -2
View File
@@ -41,7 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
ref={ref} ref={ref}
className={twMerge( className={twMerge(
`inline-flex items-center justify-between rounded-md `inline-flex items-center justify-between rounded-md
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200`, bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`,
className className
)} )}
> >
@@ -106,7 +106,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
className={twMerge( className={twMerge(
`relative mb-0.5 flex `relative mb-0.5 flex
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
outline-none transition-all hover:bg-mineshaft-500`, outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary", isSelected && "bg-primary",
isDisabled && isDisabled &&
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
@@ -9,16 +9,22 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { useProjectPermission } from "@app/context"; import { useProjectPermission } from "@app/context";
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api"; import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/types"; import { Workspace } from "@app/hooks/api/types";
import { workspaceKeys } from "@app/hooks/api/workspace/queries";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { queryClient } from "@app/reactQuery";
import { Button } from "../Button"; import { Button } from "../Button";
import { Tooltip } from "../Tooltip"; import { Tooltip } from "../Tooltip";
export type UpgradeProjectAlertProps = { export type UpgradeProjectAlertProps = {
project: Workspace; project: Workspace;
transparent?: boolean;
}; };
export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.Element | null => { export const UpgradeProjectAlert = ({
project,
transparent
}: UpgradeProjectAlertProps): JSX.Element | null => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const router = useRouter(); const router = useRouter();
const { membership } = useProjectPermission(); const { membership } = useProjectPermission();
@@ -48,6 +54,7 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
} }
if (currentStatus !== null && data?.status === null) { if (currentStatus !== null && data?.status === null) {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
router.reload(); router.reload();
} }
} }
@@ -87,10 +94,25 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
if (project.version !== ProjectVersion.V1) return null; if (project.version !== ProjectVersion.V1) return null;
if (transparent) {
return (
<Button
colorSchema="primary"
variant="solid"
size="md"
isLoading={isLoading}
isDisabled={isLoading || membership.role !== "admin"}
onClick={onUpgradeProject}
>
Upgrade
</Button>
);
}
return ( return (
<div <div
className={twMerge( className={twMerge(
"mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white", "mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
membership.role !== "admin" && "opacity-80" membership.role !== "admin" && "opacity-80"
)} )}
> >
@@ -1,10 +1,19 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import axios from "axios";
import queryString from "query-string"; import queryString from "query-string";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api"; import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2"; import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem} from "../../../components/v2";
import { import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
@@ -18,8 +27,10 @@ const cloudflareEnvironments = [
export default function CloudflarePagesIntegrationPage() { export default function CloudflarePagesIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
const { createNotification } = useNotificationContext();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const [secretPath, setSecretPath] = useState("/");
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({ const { data: integrationAuthApps } = useGetIntegrationAuthApps({
@@ -65,7 +76,7 @@ export default function CloudflarePagesIntegrationPage() {
appId: targetAppId, appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetEnvironment, targetEnvironment,
secretPath: "/" secretPath
}); });
setIsLoading(false); setIsLoading(false);
@@ -73,6 +84,18 @@ export default function CloudflarePagesIntegrationPage() {
router.push(`/integrations/${localStorage.getItem("projectData.id")}`); router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
let errorMessage: string = "Something went wrong!";
if (axios.isAxiosError(err)) {
const { message } = err?.response?.data as { message: string };
errorMessage = message;
}
createNotification({
text: errorMessage,
type: "error"
});
setIsLoading(false);
} }
}; };
@@ -106,6 +129,13 @@ export default function CloudflarePagesIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Cloudflare Pages Project" className="mt-4 px-6"> <FormControl label="Cloudflare Pages Project" className="mt-4 px-6">
<Select <Select
value={targetApp} value={targetApp}
@@ -15,7 +15,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod, useAddIdentityUniversalAuth } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup const schema = yup
@@ -40,7 +40,7 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
}; };
export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -50,6 +50,7 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: createMutateAsync } = useCreateIdentity();
const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const { const {
control, control,
@@ -112,21 +113,31 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
// create // create
const { const {
id: createdId, id: createdId
name: createdName, // name: createdName,
authMethod // authMethod
} = await createMutateAsync({ } = await createMutateAsync({
name, name,
role: role || undefined, role: role || undefined,
organizationId: orgId organizationId: orgId
}); });
handlePopUpToggle("identity", false); await addMutateAsync({
handlePopUpOpen("identityAuthMethod", { organizationId: orgId,
identityId: createdId, identityId: createdId,
name: createdName, clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
authMethod accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0
}); });
handlePopUpToggle("identity", false);
// handlePopUpOpen("identityAuthMethod", {
// identityId: createdId,
// name: createdName,
// authMethod
// });
} }
createNotification({ createNotification({
@@ -1,121 +1,36 @@
import { ProjectPermissionCan } from "@app/components/permissions"; import Link from "next/link";
import {
decryptAssymmetric, import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
encryptAssymmetric import { useWorkspace } from "@app/context";
} from "@app/components/utilities/cryptography/crypto"; import { useGetWorkspaceBot } from "@app/hooks/api";
import { Alert, AlertDescription, Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
import { ProjectVersion } from "@app/hooks/api/workspace/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types";
export const E2EESection = () => { export const E2EESection = () => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?.id ?? ""); const { data: bot } = useGetWorkspaceBot(currentWorkspace?.id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
const { data: wsKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const toggleBotActivate = async () => {
let botKey;
try {
if (!currentWorkspace?.id) return;
if (bot && wsKey) {
// case: there is a bot
if (!bot.isActive) {
// bot is not active -> activate bot
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
await updateBotActiveStatus({
workspaceId: currentWorkspace.id,
botKey,
isActive: true,
botId: bot.id
});
} else {
// bot is active -> deactivate bot
await updateBotActiveStatus({
isActive: false,
botId: bot.id,
workspaceId: currentWorkspace.id
});
}
}
} catch (err) {
console.error(err);
}
};
if (!currentWorkspace) return null; if (!currentWorkspace) return null;
return bot && currentWorkspace.version === ProjectVersion.V1 ? ( return bot && currentWorkspace.version === ProjectVersion.V1 ? (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p> <div className="flex w-full items-center justify-between">
<p className="mb-8 text-gray-400"> <p className="text-xl font-semibold">End-to-End Encryption</p>
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to <UpgradeProjectAlert transparent project={currentWorkspace} />
cloud providers as well as HTTP calls to get secrets back raw but enables the server to </div>
read/decrypt your secret values.
<p className="mt-5 max-w-2xl text-sm text-gray-400">
We are updating our encryption logic to make sure that Infisical can be the most versatile
secret management platform. <br />
<br />
Upgrading the project version is required to continue receiving the latest improvements and
patches.
</p> </p>
<p className="mb-8 text-gray-400">
Note that, even with E2EE disabled, your secrets are always encrypted at rest. <Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
</p> <a target="_blank" className="text-sm text-primary-400">
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}> Learn more about project upgrades
{(isAllowed) => ( </a>
<div className="flex w-full flex-col gap-y-3"> </Link>
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="end-to-end-encryption"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
</div>
<div>
<Alert variant="warning">
<AlertDescription>
Enabling End-to-end encryption disables all the integrations
</AlertDescription>
</Alert>
</div>
</div>
)}
</ProjectPermissionCan>
</div> </div>
) : ( ) : (
<div /> <div />
@@ -104,7 +104,7 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
Create Create
</Button> </Button>
<Button colorSchema="secondary" variant="plain"> <Button onClick={() => handlePopUpClose("createEnv")} colorSchema="secondary" variant="plain">
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -108,7 +108,7 @@ export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpTog
Update Update
</Button> </Button>
<Button colorSchema="secondary" variant="plain"> <Button onClick={() => handlePopUpClose("updateEnv")} colorSchema="secondary" variant="plain">
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -7,13 +7,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.6 version: 1.0.7
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "1.0.0" appVersion: "1.0.1"
dependencies: dependencies:
- name: ingress-nginx - name: ingress-nginx
@@ -32,7 +32,7 @@ spec:
{{- if $infisicalValues.autoDatabaseSchemaMigration }} {{- if $infisicalValues.autoDatabaseSchemaMigration }}
initContainers: initContainers:
- name: "migration-init" - name: "migration-init"
image: "groundnuty/k8s-wait-for:1.3" image: "ghcr.io/groundnuty/k8s-wait-for:no-root-v2.0"
imagePullPolicy: {{ $infisicalValues.image.pullPolicy }} imagePullPolicy: {{ $infisicalValues.image.pullPolicy }}
args: args:
- "job" - "job"
@@ -24,7 +24,7 @@ infisical:
resources: resources:
limits: limits:
memory: 350Mi memory: 600Mi
requests: requests:
cpu: 350m cpu: 350m
+2 -2
View File
@@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.4.0 version: v0.5.0
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "v0.4.0" appVersion: "v0.5.0"
@@ -90,6 +90,38 @@ spec:
- secretsScope - secretsScope
- serviceTokenSecretReference - serviceTokenSecretReference
type: object type: object
universalAuth:
properties:
credentialsRef:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret
is located
type: string
required:
- secretName
- secretNamespace
type: object
secretsScope:
properties:
envSlug:
type: string
projectSlug:
type: string
secretsPath:
type: string
required:
- envSlug
- projectSlug
- secretsPath
type: object
required:
- credentialsRef
- secretsScope
type: object
type: object type: object
hostAPI: hostAPI:
description: Infisical host to pull secrets from description: Infisical host to pull secrets from
+1 -1
View File
@@ -32,7 +32,7 @@ controllerManager:
- ALL - ALL
image: image:
repository: infisical/kubernetes-operator repository: infisical/kubernetes-operator
tag: v0.4.0 # fixed to prevent accidental upgrade tag: v0.5.0 # fixed to prevent accidental upgrade
resources: resources:
limits: limits:
cpu: 500m cpu: 500m
@@ -9,12 +9,20 @@ type Authentication struct {
ServiceAccount ServiceAccountDetails `json:"serviceAccount"` ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
// +kubebuilder:validation:Optional // +kubebuilder:validation:Optional
ServiceToken ServiceTokenDetails `json:"serviceToken"` ServiceToken ServiceTokenDetails `json:"serviceToken"`
// +kubebuilder:validation:Optional
UniversalAuth UniversalAuthDetails `json:"universalAuth"`
}
type UniversalAuthDetails struct {
// +kubebuilder:validation:Required
CredentialsRef KubeSecretReference `json:"credentialsRef"`
// +kubebuilder:validation:Required
SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"`
} }
type ServiceTokenDetails struct { type ServiceTokenDetails struct {
// +kubebuilder:validation:Required // +kubebuilder:validation:Required
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"` ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
// +kubebuilder:validation:Required // +kubebuilder:validation:Required
SecretsScope SecretScopeInWorkspace `json:"secretsScope"` SecretsScope SecretScopeInWorkspace `json:"secretsScope"`
} }
@@ -28,11 +36,19 @@ type ServiceAccountDetails struct {
type SecretScopeInWorkspace struct { type SecretScopeInWorkspace struct {
// +kubebuilder:validation:Required // +kubebuilder:validation:Required
SecretsPath string `json:"secretsPath"` SecretsPath string `json:"secretsPath"`
// +kubebuilder:validation:Required // +kubebuilder:validation:Required
EnvSlug string `json:"envSlug"` EnvSlug string `json:"envSlug"`
} }
type MachineIdentityScopeInWorkspace struct {
// +kubebuilder:validation:Required
SecretsPath string `json:"secretsPath"`
// +kubebuilder:validation:Required
EnvSlug string `json:"envSlug"`
// +kubebuilder:validation:Required
ProjectSlug string `json:"projectSlug"`
}
type KubeSecretReference struct { type KubeSecretReference struct {
// The name of the Kubernetes Secret // The name of the Kubernetes Secret
// +kubebuilder:validation:Required // +kubebuilder:validation:Required
@@ -31,6 +31,7 @@ func (in *Authentication) DeepCopyInto(out *Authentication) {
*out = *in *out = *in
out.ServiceAccount = in.ServiceAccount out.ServiceAccount = in.ServiceAccount
out.ServiceToken = in.ServiceToken out.ServiceToken = in.ServiceToken
out.UniversalAuth = in.UniversalAuth
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication.
@@ -157,6 +158,21 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MachineIdentityScopeInWorkspace) DeepCopyInto(out *MachineIdentityScopeInWorkspace) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineIdentityScopeInWorkspace.
func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWorkspace {
if in == nil {
return nil
}
out := new(MachineIdentityScopeInWorkspace)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) { func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) {
*out = *in *out = *in
@@ -219,3 +235,20 @@ func (in *ServiceTokenDetails) DeepCopy() *ServiceTokenDetails {
in.DeepCopyInto(out) in.DeepCopyInto(out)
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UniversalAuthDetails) DeepCopyInto(out *UniversalAuthDetails) {
*out = *in
out.CredentialsRef = in.CredentialsRef
out.SecretsScope = in.SecretsScope
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UniversalAuthDetails.
func (in *UniversalAuthDetails) DeepCopy() *UniversalAuthDetails {
if in == nil {
return nil
}
out := new(UniversalAuthDetails)
in.DeepCopyInto(out)
return out
}
@@ -90,6 +90,38 @@ spec:
- secretsScope - secretsScope
- serviceTokenSecretReference - serviceTokenSecretReference
type: object type: object
universalAuth:
properties:
credentialsRef:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret
is located
type: string
required:
- secretName
- secretNamespace
type: object
secretsScope:
properties:
envSlug:
type: string
projectSlug:
type: string
secretsPath:
type: string
required:
- envSlug
- projectSlug
- secretsPath
type: object
required:
- credentialsRef
- secretsScope
type: object
type: object type: object
hostAPI: hostAPI:
description: Infisical host to pull secrets from description: Infisical host to pull secrets from
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: universal-auth-credentials
type: Opaque
stringData:
clientId: <machine-identity-client-id>
clientSecret: <machine-identity-client-secret>
+37 -30
View File
@@ -1,35 +1,42 @@
apiVersion: secrets.infisical.com/v1alpha1 apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalSecret kind: InfisicalSecret
metadata: metadata:
name: infisicalsecret-sample name: infisicalsecret-sample
labels: labels:
label-to-be-passed-to-managed-secret: sample-value label-to-be-passed-to-managed-secret: sample-value
annotations: annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: 'true' example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
spec: spec:
hostAPI: http://localhost:8888/api hostAPI: https://app.infisical.com/api
resyncInterval: 10 resyncInterval: 10
authentication: authentication:
serviceAccount: # Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
serviceAccountSecretReference: # If you have multiple authentication methods defined, it may cause issues.
secretName: service-account serviceToken:
secretNamespace: default serviceTokenSecretReference:
projectId: "6439ec224cfbf7ea2a95b651" secretName: service-token
environmentName: "dev" secretNamespace: default
serviceToken: secretsScope:
serviceTokenSecretReference: envSlug: <env-slug>
secretName: service-token secretsPath: <secrets-path> # Root is "/"
secretNamespace: default
secretsScope:
envSlug: dev
secretsPath: "/"
managedSecretReference:
secretName: managed-secret
secretNamespace: default
creationPolicy: "Orphan" ## Owner | Orphan
# secretType: kubernetes.io/dockerconfigjson
# # To be depreciated soon universalAuth:
# tokenSecretReference: secretsScope:
# secretName: service-token projectSlug: <project-slug>
# secretNamespace: default envSlug: <env-slug> # "dev", "staging", "prod", etc..
secretsPath: "<secrets-path>" # Root is "/"
credentialsRef:
secretName: universal-auth-credentials
secretNamespace: default
managedSecretReference:
secretName: managed-secret
secretNamespace: default
creationPolicy: "Orphan" ## Owner | Orphan
# secretType: kubernetes.io/dockerconfigjson
# # To be depreciated soon
# tokenSecretReference:
# secretName: service-token
# secretNamespace: default
@@ -1,9 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: service-account
type: Opaque
stringData:
serviceAccountAccessKey: <>
serviceAccountPrivateKey: <>
serviceAccountPublicKey: <>
@@ -53,11 +53,11 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
if infisicalSecretCR.Spec.ResyncInterval != 0 { if infisicalSecretCR.Spec.ResyncInterval != 0 {
requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval) requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval)
fmt.Println("Manual re-sync interval set", "requeueAfter", requeueTime) fmt.Printf("\nManual re-sync interval set. Interval: %v\n", requeueTime)
} else {
fmt.Printf("\nRe-sync interval set. Interval: %v\n", requeueTime)
} }
fmt.Println("Requeue duration set", "requeueAfter", requeueTime)
// Check if the resource is already marked for deletion // Check if the resource is already marked for deletion
if infisicalSecretCR.GetDeletionTimestamp() != nil { if infisicalSecretCR.GetDeletionTimestamp() != nil {
return ctrl.Result{ return ctrl.Result{
@@ -6,7 +6,6 @@ import (
"strings" "strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/model" "github.com/Infisical/infisical/k8-operator/packages/model"
"github.com/Infisical/infisical/k8-operator/packages/util" "github.com/Infisical/infisical/k8-operator/packages/util"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@@ -20,12 +19,29 @@ const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey"
const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey" const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey"
const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey" const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey"
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken" const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config" const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config"
const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system" const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system"
const INFISICAL_DOMAIN = "https://app.infisical.com/api" const INFISICAL_DOMAIN = "https://app.infisical.com/api"
type AuthStrategyType string
var AuthStrategy = struct {
SERVICE_TOKEN AuthStrategyType
SERVICE_ACCOUNT AuthStrategyType
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
}{
SERVICE_TOKEN: "SERVICE_TOKEN",
SERVICE_ACCOUNT: "SERVICE_ACCOUNT",
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
}
var machineIdentityTokenInstance *util.MachineIdentityToken
func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) { func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) {
// default key values // default key values
defaultConfigMapData := make(map[string]string) defaultConfigMapData := make(map[string]string)
@@ -100,6 +116,28 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
} }
func (r *InfisicalSecretReconciler) GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (machineIdentityDetails model.MachineIdentityDetails, err error) {
universalAuthCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace,
Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretName,
})
if errors.IsNotFound(err) {
return model.MachineIdentityDetails{}, nil
}
if err != nil {
return model.MachineIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
}
clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID]
clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET]
return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil
}
// Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object. // Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object.
// If any keys are missing or an error occurs, returns an empty object or an error object, respectively. // If any keys are missing or an error occurs, returns an empty object or an error object, respectively.
func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) { func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
@@ -127,7 +165,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
} }
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error { func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
plainProcessedSecrets := make(map[string][]byte) plainProcessedSecrets := make(map[string][]byte)
secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType
@@ -156,7 +194,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
} }
} }
annotations[SECRET_VERSION_ANNOTATION] = encryptedSecretsResponse.ETag annotations[SECRET_VERSION_ANNOTATION] = ETag
// create a new secret as specified by the managed secret spec of CRD // create a new secret as specified by the managed secret spec of CRD
newKubeSecretInstance := &corev1.Secret{ newKubeSecretInstance := &corev1.Secret{
@@ -187,16 +225,15 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return nil return nil
} }
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error { func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
plainProcessedSecrets := make(map[string][]byte) plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI { for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) plainProcessedSecrets[secret.Key] = []byte(secret.Value)
} }
managedKubeSecret.Data = plainProcessedSecrets managedKubeSecret.Data = plainProcessedSecrets
managedKubeSecret.ObjectMeta.Annotations = map[string]string{ managedKubeSecret.ObjectMeta.Annotations = map[string]string{}
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag, managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
}
err := r.Client.Update(ctx, &managedKubeSecret) err := r.Client.Update(ctx, &managedKubeSecret)
if err != nil { if err != nil {
@@ -213,11 +250,28 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
} }
var authStrategy AuthStrategyType
serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret) serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
if err != nil { if err != nil {
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err) return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
} }
infisicalMachineIdentityCreds, err := r.GetInfisicalUniversalAuthFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
}
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
authStrategy = AuthStrategy.SERVICE_ACCOUNT
} else if infisicalToken != "" {
authStrategy = AuthStrategy.SERVICE_TOKEN
} else if infisicalMachineIdentityCreds.ClientId != "" && infisicalMachineIdentityCreds.ClientSecret != "" {
authStrategy = AuthStrategy.UNIVERSAL_MACHINE_IDENTITY
} else {
return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets")
}
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err) r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
if err != nil { if err != nil {
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err) return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
@@ -239,41 +293,60 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION] secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION]
} }
var plainTextSecretsFromApi []model.SingleEnvironmentVariable if authStrategy == AuthStrategy.UNIVERSAL_MACHINE_IDENTITY && machineIdentityTokenInstance == nil {
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response // Create new machine identity token instance
machineIdentityTokenInstance = util.NewMachineIdentityToken(infisicalMachineIdentityCreds.ClientId, infisicalMachineIdentityCreds.ClientSecret)
}
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { var plainTextSecretsFromApi []model.SingleEnvironmentVariable
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag) var updateDetails model.RequestUpdateUpdateDetails
if authStrategy == AuthStrategy.SERVICE_ACCOUNT { // Service Account
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
if err != nil { if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
} }
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account") fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
} else if infisicalToken != "" { } else if authStrategy == AuthStrategy.SERVICE_TOKEN { // Service Tokens (deprecated)
envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath) plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath)
if err != nil { if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
} }
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service token") fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service token")
} else if authStrategy == AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { // Machine Identity
accessToken, err := machineIdentityTokenInstance.GetToken()
if err != nil {
return fmt.Errorf("%s", "Waiting for access token to become available")
}
scope := infisicalSecret.Spec.Authentication.UniversalAuth.SecretsScope
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaUniversalAuth(accessToken, secretVersionBasedOnETag, scope)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via universal auth")
} else { } else {
return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets") return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets")
} }
if !fullEncryptedSecretsResponse.Modified { if !updateDetails.Modified {
fmt.Println("No secrets modified so reconcile not needed", "Etag:", fullEncryptedSecretsResponse.ETag, "Modified:", fullEncryptedSecretsResponse.Modified) fmt.Println("No secrets modified so reconcile not needed")
return nil return nil
} }
if managedKubeSecret == nil { if managedKubeSecret == nil {
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse) return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag)
} else { } else {
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse) return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag)
} }
} }
+73 -7
View File
@@ -58,7 +58,6 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
R(). R().
SetResult(&secretsResponse). SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME). SetHeader("User-Agent", USER_AGENT_NAME).
SetHeader("If-None-Match", request.ETag).
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("include_imports", "true"). // TODO needs to be set as a option SetQueryParam("include_imports", "true"). // TODO needs to be set as a option
SetQueryParam("workspaceId", request.WorkspaceId) SetQueryParam("workspaceId", request.WorkspaceId)
@@ -77,13 +76,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
} }
if response.Header().Get("etag") == request.ETag { responseETag := response.Header().Get("etag")
secretsResponse.Modified = false
} else {
secretsResponse.Modified = true
}
secretsResponse.ETag = response.Header().Get("etag") secretsResponse.Modified = request.ETag != responseETag
secretsResponse.ETag = responseETag
return secretsResponse, nil return secretsResponse, nil
} }
@@ -107,6 +103,76 @@ func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccou
return serviceAccountDetailsResponse, nil return serviceAccountDetailsResponse, nil
} }
func CallUniversalMachineIdentityLogin(request MachineIdentityUniversalAuthLoginRequest) (MachineIdentityDetailsResponse, error) {
var machineIdentityDetailsResponse MachineIdentityDetailsResponse
response, err := resty.New().
R().
SetResult(&machineIdentityDetailsResponse).
SetBody(request).
SetHeader("User-Agent", USER_AGENT_NAME).
Post(fmt.Sprintf("%v/v1/auth/universal-auth/login", API_HOST_URL))
if err != nil {
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unsuccessful response: [response=%s]", response)
}
return machineIdentityDetailsResponse, nil
}
func CallUniversalMachineIdentityRefreshAccessToken(request MachineIdentityUniversalAuthRefreshRequest) (MachineIdentityDetailsResponse, error) {
var universalAuthRefreshResponse MachineIdentityDetailsResponse
response, err := resty.New().
R().
SetResult(&universalAuthRefreshResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
SetBody(request).
Post(fmt.Sprintf("%v/v1/auth/token/renew", API_HOST_URL))
if err != nil {
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return universalAuthRefreshResponse, nil
}
func CallGetDecryptedSecretsV3(httpClient *resty.Client, request GetDecryptedSecretsV3Request) (GetDecryptedSecretsV3Response, error) {
var decryptedSecretsResponse GetDecryptedSecretsV3Response
response, err := httpClient.
R().
SetResult(&decryptedSecretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
SetQueryParam("secretPath", request.SecretPath).
SetQueryParam("workspaceSlug", request.ProjectSlug).
SetQueryParam("environment", request.Environment).
Get(fmt.Sprintf("%v/v3/secrets/raw", API_HOST_URL))
if err != nil {
return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unsuccessful response: [response=%s]", response)
}
responseETag := response.Header().Get("etag")
decryptedSecretsResponse.Modified = request.ETag != responseETag
decryptedSecretsResponse.ETag = responseETag
return decryptedSecretsResponse, nil
}
func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) { func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) {
var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions
response, err := httpClient. response, err := httpClient.
+40
View File
@@ -65,6 +65,17 @@ type EncryptedSecretV3 struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
type DecryptedSecretV3 struct {
ID string `json:"id"`
Workspace string `json:"workspace"`
Environment string `json:"environment"`
Version int `json:"version"`
Type string `json:"string"`
SecretKey string `json:"secretKey"`
SecretValue string `json:"secretValue"`
SecretComment string `json:"secretComment"`
}
type ImportedSecretV3 struct { type ImportedSecretV3 struct {
Environment string `json:"environment"` Environment string `json:"environment"`
FolderId string `json:"folderId"` FolderId string `json:"folderId"`
@@ -79,6 +90,19 @@ type GetEncryptedSecretsV3Response struct {
ETag string `json:"ETag,omitempty"` ETag string `json:"ETag,omitempty"`
} }
type GetDecryptedSecretsV3Response struct {
Secrets []DecryptedSecretV3 `json:"secrets"`
ETag string `json:"ETag,omitempty"`
Modified bool `json:"modified,omitempty"`
}
type GetDecryptedSecretsV3Request struct {
ProjectSlug string `json:"workspaceSlug"`
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
ETag string `json:"etag,omitempty"`
}
type GetServiceTokenDetailsResponse struct { type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"` ID string `json:"_id"`
Name string `json:"name"` Name string `json:"name"`
@@ -101,6 +125,13 @@ type ServiceAccountDetailsResponse struct {
} `json:"serviceAccount"` } `json:"serviceAccount"`
} }
type MachineIdentityDetailsResponse struct {
AccessToken string `json:"accessToken"`
ExpiresIn int `json:"expiresIn"`
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
TokenType string `json:"tokenType"`
}
type ServiceAccountWorkspacePermission struct { type ServiceAccountWorkspacePermission struct {
ID string `json:"_id"` ID string `json:"_id"`
ServiceAccount string `json:"serviceAccount"` ServiceAccount string `json:"serviceAccount"`
@@ -128,6 +159,15 @@ type GetServiceAccountKeysRequest struct {
ServiceAccountId string `json:"id"` ServiceAccountId string `json:"id"`
} }
type MachineIdentityUniversalAuthLoginRequest struct {
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}
type MachineIdentityUniversalAuthRefreshRequest struct {
AccessToken string `json:"accessToken"`
}
type ServiceAccountKey struct { type ServiceAccountKey struct {
ID string `json:"_id"` ID string `json:"_id"`
EncryptedKey string `json:"encryptedKey"` EncryptedKey string `json:"encryptedKey"`
+10
View File
@@ -6,6 +6,16 @@ type ServiceAccountDetails struct {
PrivateKey string PrivateKey string
} }
type MachineIdentityDetails struct {
ClientId string
ClientSecret string
}
type RequestUpdateUpdateDetails struct {
Modified bool
ETag string
}
type SingleEnvironmentVariable struct { type SingleEnvironmentVariable struct {
Key string `json:"key"` Key string `json:"key"`
Value string `json:"value"` Value string `json:"value"`
@@ -0,0 +1,170 @@
package util
import (
"fmt"
"os"
"sync"
"time"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/go-resty/resty/v2"
)
type MachineIdentityToken struct {
accessTokenTTL time.Duration
accessTokenMaxTTL time.Duration
accessTokenFetchedTime time.Time
accessTokenRefreshedTime time.Time
mutex sync.Mutex
accessToken string
clientSecret string
clientId string
}
func NewMachineIdentityToken(clientId string, clientSecret string) *MachineIdentityToken {
token := MachineIdentityToken{
clientSecret: clientSecret,
clientId: clientId,
}
go token.HandleTokenLifecycle()
return &token
}
func (t *MachineIdentityToken) HandleTokenLifecycle() error {
for {
accessTokenMaxTTLExpiresInTime := t.accessTokenFetchedTime.Add(t.accessTokenMaxTTL - (5 * time.Second))
accessTokenRefreshedTime := t.accessTokenRefreshedTime
if accessTokenRefreshedTime.IsZero() {
accessTokenRefreshedTime = t.accessTokenFetchedTime
}
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(t.accessTokenTTL - (5 * time.Second))
if t.accessTokenFetchedTime.IsZero() && t.accessTokenRefreshedTime.IsZero() {
// case: init login to get access token
fmt.Println("\nInfisical Authentication: attempting to authenticate...")
err := t.FetchNewAccessToken()
if err != nil {
fmt.Printf("\nInfisical Authentication: unable to authenticate universal auth because %v. Will retry in 30 seconds", err)
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
}
} else if time.Now().After(accessTokenMaxTTLExpiresInTime) {
fmt.Printf("\nInfisical Authentication: machine identity access token has reached max ttl, attempting to re authenticate...")
err := t.FetchNewAccessToken()
if err != nil {
fmt.Printf("\nInfisical Authentication: unable to authenticate universal auth because %v. Will retry in 30 seconds", err)
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
}
} else {
err := t.RefreshAccessToken()
if err != nil {
fmt.Printf("\nInfisical Authentication: unable to refresh universal auth token because %v. Will retry in 30 seconds", err)
// wait a bit before trying again
time.Sleep((30 * time.Second))
continue
}
}
if accessTokenRefreshedTime.IsZero() {
accessTokenRefreshedTime = t.accessTokenFetchedTime
} else {
accessTokenRefreshedTime = t.accessTokenRefreshedTime
}
nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(t.accessTokenTTL - (5 * time.Second))
accessTokenMaxTTLExpiresInTime = t.accessTokenFetchedTime.Add(t.accessTokenMaxTTL - (5 * time.Second))
if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) {
// case: Refreshed so close that the next refresh would occur beyond max ttl (this is because currently, token renew tries to add +access-token-ttl amount of time)
// example: access token ttl is 11 sec and max ttl is 30 sec. So it will start with 11 seconds, then 22 seconds but the next time you call refresh it would try to extend it to 33 but max ttl only allows 30, so the token will be valid until 30 before we need to reauth
time.Sleep(t.accessTokenTTL - nextAccessTokenExpiresInTime.Sub(accessTokenMaxTTLExpiresInTime))
} else {
time.Sleep(t.accessTokenTTL - (5 * time.Second))
}
}
}
func (t *MachineIdentityToken) RefreshAccessToken() error {
httpClient := resty.New()
httpClient.SetRetryCount(10000).
SetRetryMaxWaitTime(20 * time.Second).
SetRetryWaitTime(5 * time.Second)
accessToken, err := t.GetToken()
if err != nil {
return err
}
response, err := api.CallUniversalMachineIdentityRefreshAccessToken(api.MachineIdentityUniversalAuthRefreshRequest{AccessToken: accessToken})
if err != nil {
return err
}
accessTokenTTL := time.Duration(response.ExpiresIn * int(time.Second))
accessTokenMaxTTL := time.Duration(response.AccessTokenMaxTTL * int(time.Second))
t.accessTokenRefreshedTime = time.Now()
t.SetToken(response.AccessToken, accessTokenTTL, accessTokenMaxTTL)
return nil
}
// Fetches a new access token using client credentials
func (t *MachineIdentityToken) FetchNewAccessToken() error {
loginResponse, err := api.CallUniversalMachineIdentityLogin(api.MachineIdentityUniversalAuthLoginRequest{
ClientId: t.clientId,
ClientSecret: t.clientSecret,
})
if err != nil {
return err
}
accessTokenTTL := time.Duration(loginResponse.ExpiresIn * int(time.Second))
accessTokenMaxTTL := time.Duration(loginResponse.AccessTokenMaxTTL * int(time.Second))
if accessTokenTTL <= time.Duration(5)*time.Second {
fmt.Println("\nInfisical Authentication: At this time, k8 operator does not support refresh of tokens with 5 seconds or less ttl. Please increase access token ttl and try again")
os.Exit(1)
}
t.accessTokenFetchedTime = time.Now()
t.SetToken(loginResponse.AccessToken, accessTokenTTL, accessTokenMaxTTL)
return nil
}
func (t *MachineIdentityToken) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.accessToken = token
t.accessTokenTTL = accessTokenTTL
t.accessTokenMaxTTL = accessTokenMaxTTL
}
func (t *MachineIdentityToken) GetToken() (string, error) {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.accessToken == "" {
return "", fmt.Errorf("no machine identity access token available")
}
return t.accessToken, nil
}
+61 -20
View File
@@ -7,6 +7,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/crypto" "github.com/Infisical/infisical/k8-operator/packages/crypto"
"github.com/Infisical/infisical/k8-operator/packages/model" "github.com/Infisical/infisical/k8-operator/packages/model"
@@ -50,10 +51,44 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
return serviceTokenDetails, nil return serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) { func GetPlainTextSecretsViaUniversalAuth(accessToken string, etag string, secretScope v1alpha1.MachineIdentityScopeInWorkspace) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
httpClient := resty.New()
httpClient.SetAuthScheme("Bearer")
httpClient.SetAuthToken(accessToken)
secretsResponse, err := api.CallGetDecryptedSecretsV3(httpClient, api.GetDecryptedSecretsV3Request{
ProjectSlug: secretScope.ProjectSlug,
Environment: secretScope.EnvSlug,
SecretPath: secretScope.SecretsPath,
ETag: etag,
})
if err != nil {
return nil, model.RequestUpdateUpdateDetails{}, err
}
var secrets []model.SingleEnvironmentVariable
for _, secret := range secretsResponse.Secrets {
secrets = append(secrets, model.SingleEnvironmentVariable{
Key: secret.SecretKey,
Value: secret.SecretValue,
Type: secret.Type,
ID: secret.ID,
})
}
return secrets, model.RequestUpdateUpdateDetails{
Modified: secretsResponse.Modified,
ETag: secretsResponse.ETag,
}, nil
}
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
} }
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
@@ -65,7 +100,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
} }
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
@@ -76,51 +111,54 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
}) })
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, err return nil, model.RequestUpdateUpdateDetails{}, err
} }
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag) decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
} }
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key") return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt the required workspace key")
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets) plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, err return nil, model.RequestUpdateUpdateDetails{}, err
} }
// expand secrets that are referenced // expand secrets that are referenced
expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken) expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken)
return expandedSecrets, encryptedSecretsResponse, nil return expandedSecrets, model.RequestUpdateUpdateDetails{
Modified: encryptedSecretsResponse.Modified,
ETag: encryptedSecretsResponse.ETag,
}, nil
} }
// Fetches plaintext secrets from an API endpoint using a service account. // Fetches plaintext secrets from an API endpoint using a service account.
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key. // The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process. // Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) { func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(serviceAccountCreds.AccessKey). httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient) serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
} }
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID}) serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
} }
// find key for requested project // find key for requested project
@@ -132,28 +170,28 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
} }
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" { if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
} }
cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey) cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
} }
nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce) nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
} }
publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey) publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
} }
privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey) privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
} }
plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey) plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey)
@@ -165,15 +203,18 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
}) })
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err) return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
} }
return plainTextSecrets, encryptedSecretsResponse, nil return plainTextSecrets, model.RequestUpdateUpdateDetails{
Modified: encryptedSecretsResponse.Modified,
ETag: encryptedSecretsResponse.ETag,
}, nil
} }
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) { func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {