mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Merge branch 'main' into feat/pg-dynamic-secret
This commit is contained in:
@ -3,9 +3,6 @@
|
||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
|
||||
# JWT
|
||||
# Required secrets to sign JWT tokens
|
||||
# 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_DB=infisical
|
||||
|
||||
# Required
|
||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
|
149
.github/workflows/build-staging-and-deploy-aws.yml
vendored
Normal file
149
.github/workflows/build-staging-and-deploy-aws.yml
vendored
Normal file
@ -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
|
122
.github/workflows/build-staging-and-deploy.yml
vendored
122
.github/workflows/build-staging-and-deploy.yml
vendored
@ -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
|
@ -194,6 +194,25 @@ export const FOLDERS = {
|
||||
}
|
||||
} 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 = {
|
||||
LIST: {
|
||||
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.",
|
||||
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."
|
||||
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;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretTagsSchema } from "@app/db/schemas";
|
||||
import { SECRET_TAGS } from "@app/lib/api-docs";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -10,7 +11,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -36,12 +37,12 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim()
|
||||
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim(),
|
||||
slug: z.string().trim(),
|
||||
color: z.string()
|
||||
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
|
||||
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
|
||||
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -68,8 +69,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string().trim(),
|
||||
tagId: z.string().trim()
|
||||
projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
|
||||
tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
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 { RAW_SECRETS } from "@app/lib/api-docs";
|
||||
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
@ -23,6 +23,124 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
import { secretRawSchema } from "../sanitizedSchemas";
|
||||
|
||||
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({
|
||||
url: "/raw",
|
||||
method: "GET",
|
||||
|
@ -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 {
|
||||
if (!orgId) {
|
||||
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
|
||||
}
|
||||
|
||||
const projects = await db(TableName.ProjectMembership)
|
||||
.where(`${TableName.Project}.slug`, slug)
|
||||
.where(`${TableName.Project}.orgId`, orgId)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@ -284,10 +284,11 @@ export const projectServiceFactory = ({
|
||||
|
||||
// Get the role permission for the identity
|
||||
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||
ProjectMembershipRole.Admin,
|
||||
OrgMembershipRole.Member,
|
||||
organization.id
|
||||
);
|
||||
|
||||
// Identity has to be at least a member in order to create projects
|
||||
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||
if (!hasPrivilege)
|
||||
throw new ForbiddenRequestError({
|
||||
|
@ -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 (
|
||||
folderId: string,
|
||||
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
|
||||
@ -184,6 +205,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
bulkUpdate,
|
||||
deleteMany,
|
||||
bulkUpdateNoVersionIncrement,
|
||||
getSecretTags,
|
||||
findByFolderId,
|
||||
findByBlindIndexes
|
||||
};
|
||||
|
@ -22,6 +22,7 @@ import { TSecretDALFactory } from "./secret-dal";
|
||||
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
|
||||
import { TSecretQueueFactory } from "./secret-queue";
|
||||
import {
|
||||
TAttachSecretTagsDTO,
|
||||
TCreateBulkSecretDTO,
|
||||
TCreateSecretDTO,
|
||||
TCreateSecretRawDTO,
|
||||
@ -47,7 +48,7 @@ type TSecretServiceFactoryDep = {
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
secretVersionDAL: TSecretVersionDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
|
||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
@ -307,6 +308,7 @@ export const secretServiceFactory = ({
|
||||
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
||||
|
||||
const { secretName, ...el } = inputSecret;
|
||||
|
||||
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
||||
fnSecretBulkUpdate({
|
||||
folderId,
|
||||
@ -442,6 +444,7 @@ export const secretServiceFactory = ({
|
||||
const folderId = folder.id;
|
||||
|
||||
const secrets = await secretDAL.findByFolderId(folderId, actorId);
|
||||
|
||||
if (includeImports) {
|
||||
const secretImports = await secretImportDAL.find({ folderId });
|
||||
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
||||
@ -994,7 +997,209 @@ export const secretServiceFactory = ({
|
||||
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 {
|
||||
attachTags,
|
||||
detachTags,
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
updateSecret,
|
||||
|
@ -206,6 +206,15 @@ export type TFnSecretBulkUpdate = {
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
export type TAttachSecretTagsDTO = {
|
||||
projectSlug: string;
|
||||
secretName: string;
|
||||
tagSlugs: string[];
|
||||
environment: string;
|
||||
path: string;
|
||||
type: SecretType;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TFnSecretBulkDelete = {
|
||||
folderId: string;
|
||||
projectId: string;
|
||||
|
@ -406,14 +406,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
|
||||
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
|
||||
var secretsResponse GetEncryptedSecretsV3Response
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&secretsResponse).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
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 {
|
||||
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
|
||||
|
@ -401,7 +401,6 @@ type DeleteSecretV3Request struct {
|
||||
}
|
||||
|
||||
type UpdateSecretByNameV3Request struct {
|
||||
SecretName string `json:"secretName"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
Environment string `json:"environment"`
|
||||
Type string `json:"type"`
|
||||
|
@ -297,7 +297,6 @@ var secretsSetCmd = &cobra.Command{
|
||||
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
||||
WorkspaceID: workspaceFile.WorkspaceId,
|
||||
Environment: environmentName,
|
||||
SecretName: secret.PlainTextKey,
|
||||
SecretValueCiphertext: secret.SecretValueCiphertext,
|
||||
SecretValueIV: secret.SecretValueIV,
|
||||
SecretValueTag: secret.SecretValueTag,
|
||||
@ -305,7 +304,7 @@ var secretsSetCmd = &cobra.Command{
|
||||
SecretPath: secretsPath,
|
||||
}
|
||||
|
||||
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
|
||||
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to process secret update request")
|
||||
return
|
||||
|
4
docs/api-reference/endpoints/secret-tags/create.mdx
Normal file
4
docs/api-reference/endpoints/secret-tags/create.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/workspace/{projectId}/tags"
|
||||
---
|
4
docs/api-reference/endpoints/secret-tags/delete.mdx
Normal file
4
docs/api-reference/endpoints/secret-tags/delete.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}"
|
||||
---
|
4
docs/api-reference/endpoints/secret-tags/list.mdx
Normal file
4
docs/api-reference/endpoints/secret-tags/list.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/workspace/{projectId}/tags"
|
||||
---
|
4
docs/api-reference/endpoints/secrets/attach-tags.mdx
Normal file
4
docs/api-reference/endpoints/secrets/attach-tags.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Attach tags"
|
||||
openapi: "POST /api/v3/secrets/tags/{secretName}"
|
||||
---
|
4
docs/api-reference/endpoints/secrets/detach-tags.mdx
Normal file
4
docs/api-reference/endpoints/secrets/detach-tags.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Detach tags"
|
||||
openapi: "DELETE /api/v3/secrets/tags/{secretName}"
|
||||
---
|
@ -1,37 +1,102 @@
|
||||
---
|
||||
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
|
||||
2. Infisical will connect with your database with admin access
|
||||
3. If last rotated one was username1, then username2 is chosen to be rotated
|
||||
5. Update it's password with random value
|
||||
6. After testing it gets saved to the provided secret mapping
|
||||
1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||
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.
|
||||
|
||||
To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.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
|
||||
|
||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
||||
2. Click on `MySQL`
|
||||
3. Provide the inputs
|
||||
- Admin Username: DB admin username
|
||||
- Admin Password: DB admin password
|
||||
- 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.
|
||||
<Steps>
|
||||
<Step title="Open Secret Rotation Page">
|
||||
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||
</Step>
|
||||
<Step title="Click on MySQL card" />
|
||||
<Step title="Provide the inputs">
|
||||
<ParamField path="Admin Username" type="string" required>
|
||||
Rotator admin username
|
||||
</ParamField>
|
||||
|
||||
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"
|
||||
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.
|
||||
2. Infisical will connect with your database with admin access.
|
||||
3. If last rotated one was username1, then username2 is chosen to be rotated.
|
||||
5. Update it's password with random value.
|
||||
6. After testing it gets saved to the provided secret mapping.
|
||||
## Prerequisite
|
||||
|
||||
1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
||||
2. Click on `PostgreSQL`
|
||||
3. Provide the inputs
|
||||
- Admin Username: DB admin username
|
||||
- Admin Password: DB admin password
|
||||
- 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.
|
||||
<Steps>
|
||||
<Step title="Open Secret Rotation Page">
|
||||
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||
</Step>
|
||||
<Step title="Click on PostgresSQL card" />
|
||||
|
||||
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"
|
||||
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.
|
||||
It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys.
|
||||
Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical.
|
||||
|
||||
## Working
|
||||
## Prerequisite
|
||||
|
||||
1. Infisical will need an admin token of SendGrid to create API keys dynamically.
|
||||
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)
|
||||
You will need a valid SendGrid admin key with the necessary scope to create additional 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
|
||||
|
||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
||||
2. Click on `Twilio SendGrid Card`
|
||||
3. Provide the inputs
|
||||
- Admin API Key:
|
||||
SendGrid admin key to create lower scoped API keys.
|
||||
- API Key Scopes
|
||||
SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions)
|
||||
<Steps>
|
||||
<Step title="Open Secret Rotation Page">
|
||||
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||
</Step>
|
||||
<Step title="Click on Twilio SendGrid Card" />
|
||||
<Step title="Provide the inputs">
|
||||
<ParamField path="Admin API Key" type="string" required>
|
||||
SendGrid admin API key with permission to create child scoped API keys.
|
||||
</ParamField>
|
||||
|
||||
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.
|
||||
|
||||
Now your output mapped secret value will be replaced periodically by SendGrid.
|
||||
<ParamField path="Admin API Key" type="array" required>
|
||||
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).
|
||||
Permissions must be entered as a list of strings.
|
||||
|
||||
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.
|
||||
|
BIN
docs/images/secret-rotation/mysql-step1.png
Normal file
BIN
docs/images/secret-rotation/mysql-step1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
BIN
docs/images/secret-rotation/postgres-step1.png
Normal file
BIN
docs/images/secret-rotation/postgres-step1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
docs/images/secret-rotation/postgres-step2.png
Normal file
BIN
docs/images/secret-rotation/postgres-step2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
BIN
docs/images/secret-rotation/sendgrid-step1.png
Normal file
BIN
docs/images/secret-rotation/sendgrid-step1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
docs/images/secret-rotation/sendgrid-step2.png
Normal file
BIN
docs/images/secret-rotation/sendgrid-step2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme
|
||||
|
||||
## 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>
|
||||
<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
|
||||
kind: InfisicalSecret
|
||||
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:
|
||||
# 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
|
||||
resyncInterval: 60
|
||||
authentication:
|
||||
serviceToken:
|
||||
serviceTokenSecretReference:
|
||||
secretName: service-token
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval: 10
|
||||
authentication:
|
||||
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||
# If you have multiple authentication methods defined, it may cause issues.
|
||||
universalAuth:
|
||||
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
|
||||
secretsScope:
|
||||
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
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
```
|
||||
### InfisicalSecret CRD properties
|
||||
|
||||
@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute.
|
||||
</Accordion>
|
||||
|
||||
<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 title="authentication.serviceToken.serviceTokenSecretReference">
|
||||
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.
|
||||
<Accordion title="authentication.universalAuth">
|
||||
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.
|
||||
|
||||
#### 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.
|
||||
|
||||
``` 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
|
||||
|
||||
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
|
||||
```yaml
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute.
|
||||
serviceTokenSecretReference:
|
||||
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
|
||||
secretsScope:
|
||||
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||
secretsPath: <secrets-path> # Root is "/"
|
||||
...
|
||||
```
|
||||
</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">
|
||||
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.
|
||||
|
@ -467,6 +467,14 @@
|
||||
"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",
|
||||
"pages": [
|
||||
@ -474,7 +482,9 @@
|
||||
"api-reference/endpoints/secrets/create",
|
||||
"api-reference/endpoints/secrets/read",
|
||||
"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);
|
||||
|
||||
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("}");
|
||||
if (isInterpolationSyntax) {
|
||||
skipNext = true;
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
||||
${<span className="ph-no-capture text-yello-200/80">{el.slice(2, -1)}</span>
|
||||
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</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
|
||||
// when break is added a line break works properly
|
||||
return formatedContent.concat(<br />);
|
||||
return formattedContent.concat(<br />);
|
||||
};
|
||||
|
||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
|
@ -41,7 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
`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
|
||||
)}
|
||||
>
|
||||
@ -106,7 +106,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
className={twMerge(
|
||||
`relative mb-0.5 flex
|
||||
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",
|
||||
isDisabled &&
|
||||
"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 { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
import { workspaceKeys } from "@app/hooks/api/workspace/queries";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
import { queryClient } from "@app/reactQuery";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Tooltip } from "../Tooltip";
|
||||
|
||||
export type UpgradeProjectAlertProps = {
|
||||
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 router = useRouter();
|
||||
const { membership } = useProjectPermission();
|
||||
@ -48,6 +54,7 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
|
||||
}
|
||||
|
||||
if (currentStatus !== null && data?.status === null) {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
router.reload();
|
||||
}
|
||||
}
|
||||
@ -87,10 +94,25 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
|
||||
|
||||
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 (
|
||||
<div
|
||||
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"
|
||||
)}
|
||||
>
|
||||
|
@ -1,10 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import axios from "axios";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
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 {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@ -18,8 +27,10 @@ const cloudflareEnvironments = [
|
||||
export default function CloudflarePagesIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||
@ -65,7 +76,7 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
appId: targetAppId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetEnvironment,
|
||||
secretPath: "/"
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@ -73,6 +84,18 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||
} catch (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>
|
||||
</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">
|
||||
<Select
|
||||
value={targetApp}
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
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";
|
||||
|
||||
const schema = yup
|
||||
@ -40,7 +40,7 @@ type Props = {
|
||||
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 { currentOrg } = useOrganization();
|
||||
@ -50,6 +50,7 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||
|
||||
const {
|
||||
control,
|
||||
@ -112,21 +113,31 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
|
||||
// create
|
||||
|
||||
const {
|
||||
id: createdId,
|
||||
name: createdName,
|
||||
authMethod
|
||||
id: createdId
|
||||
// name: createdName,
|
||||
// authMethod
|
||||
} = await createMutateAsync({
|
||||
name,
|
||||
role: role || undefined,
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
handlePopUpToggle("identity", false);
|
||||
handlePopUpOpen("identityAuthMethod", {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId: createdId,
|
||||
name: createdName,
|
||||
authMethod
|
||||
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||
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({
|
||||
|
@ -1,121 +1,36 @@
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { Alert, AlertDescription, Checkbox } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||
import Link from "next/link";
|
||||
|
||||
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetWorkspaceBot } from "@app/hooks/api";
|
||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const E2EESection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
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;
|
||||
|
||||
return bot && currentWorkspace.version === ProjectVersion.V1 ? (
|
||||
<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>
|
||||
<p className="mb-8 text-gray-400">
|
||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
|
||||
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
|
||||
read/decrypt your secret values.
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">End-to-End Encryption</p>
|
||||
<UpgradeProjectAlert transparent project={currentWorkspace} />
|
||||
</div>
|
||||
|
||||
<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 className="mb-8 text-gray-400">
|
||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
||||
</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||
{(isAllowed) => (
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
<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>
|
||||
|
||||
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
|
||||
<a target="_blank" className="text-sm text-primary-400">
|
||||
Learn more about project upgrades
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
|
@ -104,7 +104,7 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
|
||||
Create
|
||||
</Button>
|
||||
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
<Button onClick={() => handlePopUpClose("createEnv")} colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -108,7 +108,7 @@ export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpTog
|
||||
Update
|
||||
</Button>
|
||||
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
<Button onClick={() => handlePopUpClose("updateEnv")} colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -7,13 +7,13 @@ type: application
|
||||
# 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.
|
||||
# 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
|
||||
# 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.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.0.0"
|
||||
appVersion: "1.0.1"
|
||||
|
||||
dependencies:
|
||||
- name: ingress-nginx
|
||||
|
@ -32,7 +32,7 @@ spec:
|
||||
{{- if $infisicalValues.autoDatabaseSchemaMigration }}
|
||||
initContainers:
|
||||
- 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 }}
|
||||
args:
|
||||
- "job"
|
||||
|
@ -24,7 +24,7 @@ infisical:
|
||||
|
||||
resources:
|
||||
limits:
|
||||
memory: 350Mi
|
||||
memory: 600Mi
|
||||
requests:
|
||||
cpu: 350m
|
||||
|
||||
|
@ -13,9 +13,9 @@ type: application
|
||||
# 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.
|
||||
# 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
|
||||
# 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.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.4.0"
|
||||
appVersion: "v0.5.0"
|
||||
|
@ -90,6 +90,38 @@ spec:
|
||||
- secretsScope
|
||||
- serviceTokenSecretReference
|
||||
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
|
||||
hostAPI:
|
||||
description: Infisical host to pull secrets from
|
||||
|
@ -32,7 +32,7 @@ controllerManager:
|
||||
- ALL
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.4.0 # fixed to prevent accidental upgrade
|
||||
tag: v0.5.0 # fixed to prevent accidental upgrade
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
|
@ -9,12 +9,20 @@ type Authentication struct {
|
||||
ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
|
||||
// +kubebuilder:validation:Optional
|
||||
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 {
|
||||
// +kubebuilder:validation:Required
|
||||
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
|
||||
|
||||
// +kubebuilder:validation:Required
|
||||
SecretsScope SecretScopeInWorkspace `json:"secretsScope"`
|
||||
}
|
||||
@ -28,11 +36,19 @@ type ServiceAccountDetails struct {
|
||||
type SecretScopeInWorkspace struct {
|
||||
// +kubebuilder:validation:Required
|
||||
SecretsPath string `json:"secretsPath"`
|
||||
|
||||
// +kubebuilder:validation:Required
|
||||
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 {
|
||||
// The name of the Kubernetes Secret
|
||||
// +kubebuilder:validation:Required
|
||||
|
@ -31,6 +31,7 @@ func (in *Authentication) DeepCopyInto(out *Authentication) {
|
||||
*out = *in
|
||||
out.ServiceAccount = in.ServiceAccount
|
||||
out.ServiceToken = in.ServiceToken
|
||||
out.UniversalAuth = in.UniversalAuth
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication.
|
||||
@ -157,6 +158,21 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference {
|
||||
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.
|
||||
func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) {
|
||||
*out = *in
|
||||
@ -219,3 +235,20 @@ func (in *ServiceTokenDetails) DeepCopy() *ServiceTokenDetails {
|
||||
in.DeepCopyInto(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
|
||||
- serviceTokenSecretReference
|
||||
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
|
||||
hostAPI:
|
||||
description: Infisical host to pull secrets from
|
||||
|
8
k8-operator/config/samples/machineIdentitySecret.yaml
Normal file
8
k8-operator/config/samples/machineIdentitySecret.yaml
Normal file
@ -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>
|
@ -1,35 +1,42 @@
|
||||
apiVersion: secrets.infisical.com/v1alpha1
|
||||
kind: InfisicalSecret
|
||||
metadata:
|
||||
name: infisicalsecret-sample
|
||||
labels:
|
||||
label-to-be-passed-to-managed-secret: sample-value
|
||||
annotations:
|
||||
reflector.v1.k8s.emberstack.com/reflection-allowed: 'true'
|
||||
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:
|
||||
hostAPI: http://localhost:8888/api
|
||||
resyncInterval: 10
|
||||
authentication:
|
||||
serviceAccount:
|
||||
serviceAccountSecretReference:
|
||||
secretName: service-account
|
||||
secretNamespace: default
|
||||
projectId: "6439ec224cfbf7ea2a95b651"
|
||||
environmentName: "dev"
|
||||
serviceToken:
|
||||
serviceTokenSecretReference:
|
||||
secretName: service-token
|
||||
secretNamespace: default
|
||||
secretsScope:
|
||||
envSlug: dev
|
||||
secretsPath: "/"
|
||||
managedSecretReference:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan" ## Owner | Orphan
|
||||
# secretType: kubernetes.io/dockerconfigjson
|
||||
hostAPI: https://app.infisical.com/api
|
||||
resyncInterval: 10
|
||||
authentication:
|
||||
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||
# If you have multiple authentication methods defined, it may cause issues.
|
||||
serviceToken:
|
||||
serviceTokenSecretReference:
|
||||
secretName: service-token
|
||||
secretNamespace: default
|
||||
secretsScope:
|
||||
envSlug: <env-slug>
|
||||
secretsPath: <secrets-path> # Root is "/"
|
||||
|
||||
# # To be depreciated soon
|
||||
# tokenSecretReference:
|
||||
# secretName: service-token
|
||||
# secretNamespace: default
|
||||
universalAuth:
|
||||
secretsScope:
|
||||
projectSlug: <project-slug>
|
||||
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 {
|
||||
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
|
||||
if infisicalSecretCR.GetDeletionTimestamp() != nil {
|
||||
return ctrl.Result{
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/util"
|
||||
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_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 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_NAMESPACE = "infisical-operator-system"
|
||||
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) {
|
||||
// default key values
|
||||
defaultConfigMapData := make(map[string]string)
|
||||
@ -100,6 +116,28 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.
|
||||
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.
|
||||
// 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) {
|
||||
@ -127,7 +165,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
|
||||
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)
|
||||
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
|
||||
newKubeSecretInstance := &corev1.Secret{
|
||||
@ -187,16 +225,15 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
|
||||
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)
|
||||
for _, secret := range secretsFromAPI {
|
||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
|
||||
}
|
||||
|
||||
managedKubeSecret.Data = plainProcessedSecrets
|
||||
managedKubeSecret.ObjectMeta.Annotations = map[string]string{
|
||||
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
|
||||
}
|
||||
managedKubeSecret.ObjectMeta.Annotations = map[string]string{}
|
||||
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
|
||||
|
||||
err := r.Client.Update(ctx, &managedKubeSecret)
|
||||
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)
|
||||
}
|
||||
|
||||
var authStrategy AuthStrategyType
|
||||
|
||||
serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
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]
|
||||
}
|
||||
|
||||
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
|
||||
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response
|
||||
if authStrategy == AuthStrategy.UNIVERSAL_MACHINE_IDENTITY && machineIdentityTokenInstance == nil {
|
||||
// Create new machine identity token instance
|
||||
machineIdentityTokenInstance = util.NewMachineIdentityToken(infisicalMachineIdentityCreds.ClientId, infisicalMachineIdentityCreds.ClientSecret)
|
||||
}
|
||||
|
||||
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
|
||||
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
|
||||
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
|
||||
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 {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
fmt.Println("No secrets modified so reconcile not needed", "Etag:", fullEncryptedSecretsResponse.ETag, "Modified:", fullEncryptedSecretsResponse.Modified)
|
||||
if !updateDetails.Modified {
|
||||
fmt.Println("No secrets modified so reconcile not needed")
|
||||
return nil
|
||||
}
|
||||
|
||||
if managedKubeSecret == nil {
|
||||
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
|
||||
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag)
|
||||
} else {
|
||||
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
|
||||
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -58,7 +58,6 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
|
||||
R().
|
||||
SetResult(&secretsResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
SetHeader("If-None-Match", request.ETag).
|
||||
SetQueryParam("environment", request.Environment).
|
||||
SetQueryParam("include_imports", "true"). // TODO needs to be set as a option
|
||||
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)
|
||||
}
|
||||
|
||||
if response.Header().Get("etag") == request.ETag {
|
||||
secretsResponse.Modified = false
|
||||
} else {
|
||||
secretsResponse.Modified = true
|
||||
}
|
||||
responseETag := response.Header().Get("etag")
|
||||
|
||||
secretsResponse.ETag = response.Header().Get("etag")
|
||||
secretsResponse.Modified = request.ETag != responseETag
|
||||
secretsResponse.ETag = responseETag
|
||||
|
||||
return secretsResponse, nil
|
||||
}
|
||||
@ -107,6 +103,76 @@ func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccou
|
||||
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) {
|
||||
var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions
|
||||
response, err := httpClient.
|
||||
|
@ -65,6 +65,17 @@ type EncryptedSecretV3 struct {
|
||||
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 {
|
||||
Environment string `json:"environment"`
|
||||
FolderId string `json:"folderId"`
|
||||
@ -79,6 +90,19 @@ type GetEncryptedSecretsV3Response struct {
|
||||
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 {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
@ -101,6 +125,13 @@ type ServiceAccountDetailsResponse struct {
|
||||
} `json:"serviceAccount"`
|
||||
}
|
||||
|
||||
type MachineIdentityDetailsResponse struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
ExpiresIn int `json:"expiresIn"`
|
||||
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
|
||||
TokenType string `json:"tokenType"`
|
||||
}
|
||||
|
||||
type ServiceAccountWorkspacePermission struct {
|
||||
ID string `json:"_id"`
|
||||
ServiceAccount string `json:"serviceAccount"`
|
||||
@ -128,6 +159,15 @@ type GetServiceAccountKeysRequest struct {
|
||||
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 {
|
||||
ID string `json:"_id"`
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
|
@ -6,6 +6,16 @@ type ServiceAccountDetails struct {
|
||||
PrivateKey string
|
||||
}
|
||||
|
||||
type MachineIdentityDetails struct {
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
type RequestUpdateUpdateDetails struct {
|
||||
Modified bool
|
||||
ETag string
|
||||
}
|
||||
|
||||
type SingleEnvironmentVariable struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
|
170
k8-operator/packages/util/machine-identity-token.go
Normal file
170
k8-operator/packages/util/machine-identity-token.go
Normal file
@ -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
|
||||
}
|
@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/crypto"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/model"
|
||||
@ -50,10 +51,44 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
|
||||
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)
|
||||
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])
|
||||
@ -65,7 +100,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
|
||||
|
||||
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||
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{
|
||||
@ -76,51 +111,54 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, err
|
||||
return nil, model.RequestUpdateUpdateDetails{}, err
|
||||
}
|
||||
|
||||
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, err
|
||||
return nil, model.RequestUpdateUpdateDetails{}, err
|
||||
}
|
||||
|
||||
// expand secrets that are referenced
|
||||
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.
|
||||
// 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.
|
||||
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.SetAuthToken(serviceAccountCreds.AccessKey).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
|
||||
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})
|
||||
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
|
||||
@ -132,28 +170,28 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
@ -165,15 +203,18 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
|
||||
})
|
||||
|
||||
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)
|
||||
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) {
|
||||
|
Reference in New Issue
Block a user