Compare commits

...

87 Commits

Author SHA1 Message Date
Daniel Hougaard
c3c0006a25 Update project-service.ts 2024-03-24 17:15:10 +01:00
Maidul Islam
2241908d0a fix lining for gha 2024-03-24 00:52:21 -04:00
Maidul Islam
59b822510c update job names gha 2024-03-24 00:50:40 -04:00
Maidul Islam
d1408aff35 update pipeline 2024-03-24 00:49:47 -04:00
Maidul Islam
c67084f08d combine migration job with deploy 2024-03-24 00:47:46 -04:00
Maidul Islam
a280e002ed add prod deploy 2024-03-24 00:35:10 -04:00
Maidul Islam
76c4a8660f Merge pull request #1621 from redcubie/patch-1
Move DB_CONNECTION_URI to make sure DB credentials are initialized
2024-03-23 12:59:27 -04:00
redcubie
8c54dd611e Move DB_CONNECTION_URI to make sure DB credentials are initialized 2024-03-23 18:49:15 +02:00
Maidul Islam
5c75f526e7 Update build-staging-and-deploy-aws.yml 2024-03-22 17:18:45 -04:00
Maidul Islam
113e777b25 add wait for ecs 2024-03-22 16:16:22 -04:00
Maidul Islam
2a93449ffe add needs[] for gamm deploy gha 2024-03-22 14:56:38 -04:00
Maidul Islam
1ef1c042da add back other build steps 2024-03-22 14:55:55 -04:00
Maidul Islam
44ca8c315e remove all stages except deploy in gha 2024-03-22 14:12:13 -04:00
Daniel Hougaard
7766a7f4dd Merge pull request #1619 from Infisical/daniel/mi-ux-fix
Update IdentityModal.tsx
2024-03-22 18:56:07 +01:00
Daniel Hougaard
3cb150a749 Update IdentityModal.tsx 2024-03-22 18:27:57 +01:00
Maidul Islam
9e9ce261c8 give gha permission to update git token 2024-03-22 12:59:51 -04:00
Maidul Islam
fab7167850 update oidc audience 2024-03-22 12:54:27 -04:00
Akhil Mohan
c7de9aab4e Merge pull request #1618 from Infisical/gha-aws-pipeline
deploy to ecs using OIDC with aws
2024-03-22 22:13:09 +05:30
Maidul Islam
3560346f85 update step name 2024-03-22 12:42:32 -04:00
Maidul Islam
f0bf2f8dd0 seperate aws rds uri 2024-03-22 12:38:10 -04:00
Maidul Islam
2a6216b8fc deploy to ecs using OIDC with aws 2024-03-22 12:29:07 -04:00
Akhil Mohan
c05230f667 Merge pull request #1616 from Infisical/wait-for-job-helm
Update Chart.yaml
2024-03-22 19:03:32 +05:30
Maidul Islam
d68055a264 Update Chart.yaml
Update to multi arch and rootless
2024-03-22 09:28:44 -04:00
Maidul Islam
dc6056b564 Merge pull request #1614 from francodalmau/fix-environment-popups-cancel-action
Fix add and update environment popups cancel button
2024-03-21 21:28:28 -04:00
franco_dalmau
94f0811661 Fix add and update environment popups cancel button 2024-03-21 20:09:57 -03:00
Maidul Islam
7b84ae6173 Update Chart.yaml 2024-03-21 15:08:38 -04:00
Maidul Islam
5710a304f8 Merge pull request #1533 from Infisical/daniel/k8-operator-machine-identities
Feat: K8 Operator Machine Identity Support
2024-03-21 15:01:50 -04:00
Daniel Hougaard
91e3bbba34 Fix: Requested changes 2024-03-21 19:58:10 +01:00
Daniel Hougaard
02112ede07 Fix: Requested changes 2024-03-21 19:53:21 +01:00
Daniel Hougaard
08cfbf64e4 Fix: Error handing 2024-03-21 19:37:12 +01:00
Daniel Hougaard
18da522b45 Chore: Helm charts 2024-03-21 18:35:00 +01:00
Daniel Hougaard
8cf68fbd9c Generated 2024-03-21 17:12:42 +01:00
Daniel Hougaard
d6b82dfaa4 Fix: Rebase sample conflicts 2024-03-21 17:09:41 +01:00
Daniel Hougaard
7bd4eed328 Chore: Generate K8 helm charts 2024-03-21 17:08:01 +01:00
Daniel Hougaard
0341c32da0 Fix: Change credentials -> credentialsRef 2024-03-21 17:08:01 +01:00
Daniel Hougaard
caea055281 Feat: Improve K8 docs 2024-03-21 17:08:01 +01:00
Daniel Hougaard
c08c78de8d Feat: Rename universalAuthMachineIdentity to universalAuth 2024-03-21 17:08:01 +01:00
Daniel Hougaard
3765a14246 Fix: Generate new types 2024-03-21 17:08:01 +01:00
Daniel Hougaard
c5a11e839b Feat: Deprecate Service Accounts 2024-03-21 17:08:01 +01:00
Daniel Hougaard
93bd3d8270 Docs: Simplified docs more 2024-03-21 17:07:56 +01:00
Daniel Hougaard
b9601dd418 Update kubernetes.mdx 2024-03-21 17:07:56 +01:00
Daniel Hougaard
ae3bc04b07 Docs 2024-03-21 17:07:31 +01:00
Daniel Hougaard
11edefa66f Feat: Added project slug support 2024-03-21 17:07:31 +01:00
Daniel Hougaard
f71459ede0 Slugs 2024-03-21 17:07:31 +01:00
Daniel Hougaard
33324a5a3c Type generation 2024-03-21 17:07:31 +01:00
Daniel Hougaard
5c6781a705 Update machine-identity-token.go 2024-03-21 17:07:31 +01:00
Daniel Hougaard
71e31518d7 Feat: Add machine identity token handler 2024-03-21 17:07:31 +01:00
Daniel Hougaard
f6f6db2898 Fix: Moved update attributes type to models 2024-03-21 17:07:31 +01:00
Daniel Hougaard
55780b65d3 Feat: Machine Identity support (token refreshing logic) 2024-03-21 17:07:31 +01:00
Daniel Hougaard
83bbf9599d Feat: Machine Identity support 2024-03-21 17:07:31 +01:00
Daniel Hougaard
f8f2b2574d Feat: Machine Identity support (types) 2024-03-21 17:07:31 +01:00
Daniel Hougaard
318d12addd Feat: Machine Identity support 2024-03-21 17:07:31 +01:00
Daniel Hougaard
872a28d02a Feat: Machine Identity support for K8 2024-03-21 17:06:49 +01:00
Daniel Hougaard
6f53a5631c Fix: Double prints 2024-03-21 17:06:49 +01:00
Daniel Hougaard
ff2098408d Update sample.yaml 2024-03-21 17:06:48 +01:00
Daniel Hougaard
9e85d9bbf0 Example 2024-03-21 17:05:46 +01:00
Daniel Hougaard
0f3a48bb32 Generated 2024-03-21 17:05:46 +01:00
Daniel Hougaard
f869def8ea Added new types 2024-03-21 17:05:46 +01:00
Maidul Islam
378bc57a88 Merge pull request #1480 from akhilmhdh/docs/rotation-doc-update
docs: improved secret rotation documentation with better understanding
2024-03-21 11:17:19 -04:00
Maidul Islam
242179598b fix types, rephrase, and revise rotation docs 2024-03-21 11:03:41 -04:00
Maidul Islam
e3e049b66c Update build-staging-and-deploy.yml 2024-03-20 22:14:46 -04:00
Maidul Islam
878e4a79e7 Merge pull request #1606 from Infisical/daniel/ui-imported-folders-fix
Fix: UI indicator for imports
2024-03-20 22:08:50 -04:00
Daniel Hougaard
609ce8e5cc Fix: Improved UI import indicators 2024-03-21 03:06:14 +01:00
Maidul Islam
04c1ea9b11 Update build-staging-and-deploy.yml 2024-03-20 18:03:49 -04:00
Maidul Islam
3baca73e53 add seperate step for ecr build 2024-03-20 16:25:54 -04:00
Daniel Hougaard
36adf6863b Fix: UI secret import indicator 2024-03-20 21:09:54 +01:00
Daniel Hougaard
6363e7d30a Update index.ts 2024-03-20 20:26:28 +01:00
Daniel Hougaard
f9621fad8e Fix: Remove duplicate type 2024-03-20 20:26:00 +01:00
Daniel Hougaard
90be28b87a Feat: Import indicator 2024-03-20 20:25:48 +01:00
Daniel Hougaard
671adee4d7 Feat: Indicator for wether or not secrets are imported 2024-03-20 20:24:06 +01:00
Daniel Hougaard
c9cb90c98e Feat: Add center property to tooltip 2024-03-20 20:23:21 +01:00
Daniel Hougaard
08b79d65ea Fix: Remove unused lint disable 2024-03-20 14:55:02 +01:00
Daniel Hougaard
4e1733ba6c Fix: More reverting 2024-03-20 14:44:40 +01:00
Daniel Hougaard
a4e495ea1c Fix: Restructured frontend 2024-03-20 14:42:34 +01:00
Daniel Hougaard
a750d68363 Fix: Reverted backend changes 2024-03-20 14:40:24 +01:00
Daniel Hougaard
d7161a353d Fix: Better variable naming 2024-03-20 13:15:51 +01:00
Daniel Hougaard
12c414817f Fix: Remove debugging logs 2024-03-20 13:14:18 +01:00
Daniel Hougaard
e5e494d0ee Fix: Also display imported folder indicator for nested folders 2024-03-20 13:13:50 +01:00
Daniel Hougaard
5a21b85e9e Fix: Removed overlap from other working branch 2024-03-20 13:13:19 +01:00
Daniel Hougaard
348fdf6429 Feat: Visualize imported folders in overview page (include imported folders in response) 2024-03-20 12:56:11 +01:00
Daniel Hougaard
88e609cb66 Feat: New types for imported folders 2024-03-20 12:55:40 +01:00
Daniel Hougaard
78058d691a Enhancement: Add disabled prop to Tooltip component 2024-03-20 12:55:08 +01:00
Daniel Hougaard
1d465a50c3 Feat: Visualize imported folders in overview page 2024-03-20 12:54:44 +01:00
Akhil Mohan
8c491668dc docs: updated images of inputs in secret rotation 2024-03-07 23:17:32 +05:30
Akhil Mohan
c873e2cba8 docs: updated secret rotation doc with images 2024-03-05 15:42:53 +05:30
Maidul Islam
1bc045a7fa update overview sendgrid 2024-03-05 15:42:53 +05:30
Akhil Mohan
533de93199 docs: improved secret rotation documentation with better understanding 2024-03-05 15:42:53 +05:30
45 changed files with 1397 additions and 243 deletions

View File

@@ -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

View 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.extract_version.outputs.version }}
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::135906656851:role/github-action-deploy-prod
- 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-migration:
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::135906656851:role/github-action-deploy-prod
- 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

View File

@@ -46,9 +46,6 @@ jobs:
tags: infisical/infisical:test
- name: 🏗️ Build backend and push to docker hub
uses: depot/build-push-action@v1
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
@@ -58,14 +55,11 @@ jobs:
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest
${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ steps.commit.outputs.short }}
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

View File

@@ -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({

View File

@@ -232,6 +232,7 @@ export const secretFolderServiceFactory = ({
if (!parentFolder) return [];
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
return folders;
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -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.

View File

@@ -14,7 +14,8 @@ const replaceContentWithDot = (str: string) => {
return finalStr;
};
const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
if (isImport) return "IMPORTED";
if (content === "") return "EMPTY";
if (!content) return "EMPTY";
if (!isVisible) return replaceContentWithDot(content);
@@ -46,6 +47,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
value?: string | null;
isVisible?: boolean;
isImport?: boolean;
isReadOnly?: boolean;
isDisabled?: boolean;
containerClassName?: string;
@@ -55,7 +57,17 @@ const commonClassName = "font-mono text-sm caret-white border-none outline-none
export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
(
{ value, isVisible, containerClassName, onBlur, isDisabled, isReadOnly, onFocus, ...props },
{
value,
isVisible,
isImport,
containerClassName,
onBlur,
isDisabled,
isReadOnly,
onFocus,
...props
},
ref
) => {
const [isSecretFocused, setIsSecretFocused] = useToggle();
@@ -69,7 +81,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
<pre aria-hidden className="m-0 ">
<code className={`inline-block w-full ${commonClassName}`}>
<span style={{ whiteSpace: "break-spaces" }}>
{syntaxHighlight(value, isVisible || isSecretFocused)}
{syntaxHighlight(value, isVisible || isSecretFocused, isImport)}
</span>
</code>
</pre>

View File

@@ -11,6 +11,8 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
onOpenChange?: (isOpen: boolean) => void;
defaultOpen?: boolean;
position?: "top" | "bottom" | "left" | "right";
isDisabled?: boolean;
center?: boolean;
};
export const Tooltip = ({
@@ -20,7 +22,9 @@ export const Tooltip = ({
onOpenChange,
defaultOpen,
className,
center,
asChild = true,
isDisabled,
position = "top",
...props
}: TooltipProps) => (
@@ -38,11 +42,13 @@ export const Tooltip = ({
{...props}
className={twMerge(
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
`,
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
`,
isDisabled && "!hidden",
center && "text-center",
className
)}
>

View File

@@ -1,2 +1,7 @@
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
export { useGetImportedSecrets, useGetSecretImports } from "./queries";
export {
useGetImportedFoldersByEnv,
useGetImportedSecretsAllEnvs,
useGetImportedSecretsSingleEnv,
useGetSecretImports
} from "./queries";

View File

@@ -1,5 +1,5 @@
import { useCallback } from "react";
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query";
import {
decryptAssymmetric,
@@ -7,7 +7,15 @@ import {
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImport } from "./types";
import {
TGetImportedFoldersByEnvDTO,
TGetImportedSecrets,
TGetSecretImports,
TGetSecretImportsAllEnvs,
TImportedSecrets,
TSecretImport,
TuseGetImportedFoldersByEnv
} from "./types";
export const secretImportKeys = {
getProjectSecretImports: ({ environment, projectId, path }: TGetSecretImports) =>
@@ -17,7 +25,11 @@ export const secretImportKeys = {
projectId,
path
}: Omit<TGetImportedSecrets, "decryptFileKey">) =>
[{ environment, path, projectId }, "secrets-import-sec"] as const
[{ environment, path, projectId }, "secrets-import-sec"] as const,
getImportedFoldersByEnv: ({ environment, projectId, path }: TGetImportedFoldersByEnvDTO) =>
[{ environment, projectId, path }, "imported-folders"] as const,
getImportedFoldersAllEnvs: ({ projectId, path, environment }: TGetImportedFoldersByEnvDTO) =>
[{ projectId, path, environment }, "imported-folders-all-envs"] as const
};
const fetchSecretImport = async ({ projectId, environment, path = "/" }: TGetSecretImports) => {
@@ -75,7 +87,25 @@ const fetchImportedSecrets = async (
return data.secrets;
};
export const useGetImportedSecrets = ({
const fetchImportedFolders = async ({
projectId,
environment,
path
}: TGetImportedFoldersByEnvDTO) => {
const { data } = await apiRequest.get<{ secretImports: TSecretImport[] }>(
"/api/v1/secret-imports",
{
params: {
workspaceId: projectId,
environment,
path
}
}
);
return data.secretImports;
};
export const useGetImportedSecretsSingleEnv = ({
environment,
decryptFileKey,
path,
@@ -159,3 +189,138 @@ export const useGetImportedSecrets = ({
[decryptFileKey]
)
});
export const useGetImportedSecretsAllEnvs = ({
projectId,
environments,
path = "/",
decryptFileKey
}: TGetSecretImportsAllEnvs) => {
const secretImports = useQueries({
queries: environments.map((env) => ({
queryKey: secretImportKeys.getImportedFoldersAllEnvs({
environment: env,
projectId,
path
}),
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
enabled: Boolean(projectId) && Boolean(env),
// eslint-disable-next-line react-hooks/rules-of-hooks
select: useCallback(
(data: TImportedSecrets[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data.map((el) => ({
environment: el.environment,
secretPath: el.secretPath,
environmentInfo: el.environmentInfo,
folderId: el.folderId,
secrets: el.secrets.map((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
return {
id: encSecret.id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version
};
})
}));
},
[decryptFileKey]
)
}))
});
const isImportedSecretPresentInEnv = useCallback(
(secPath: string, envSlug: string, secretName: string) => {
const selectedEnvIndex = environments.indexOf(envSlug);
if (selectedEnvIndex !== -1) {
const isPresent = secretImports?.[selectedEnvIndex]?.data?.find(
({ secretPath, secrets }) =>
secretPath === secPath && secrets.some((s) => s.key === secretName)
);
return Boolean(isPresent);
}
return false;
},
[(secretImports || []).map((response) => response.data)]
);
return { secretImports, isImportedSecretPresentInEnv };
};
export const useGetImportedFoldersByEnv = ({
projectId,
environments,
path = "/"
}: TuseGetImportedFoldersByEnv) => {
const queryParams = new URLSearchParams(window.location.search);
const currentPath = path;
const importedFolders = useQueries({
queries: environments.map((env) => ({
queryKey: secretImportKeys.getImportedFoldersByEnv({
projectId,
environment: env,
path: currentPath
}),
queryFn: async () => fetchImportedFolders({ projectId, environment: env, path: currentPath }),
enabled: Boolean(projectId) && Boolean(env)
}))
});
const isImportedFolderPresentInEnv = useCallback(
(name: string, env: string) => {
const selectedEnvIndex = environments.indexOf(env);
if (selectedEnvIndex !== -1) {
const currentlyBrowsingPath = queryParams.get("secretPath") || "";
const isPresent = importedFolders?.[selectedEnvIndex]?.data?.find(
({ importPath }) => importPath === `${currentlyBrowsingPath}/${name}`
);
return Boolean(isPresent);
}
return false;
},
[(importedFolders || []).map((response) => response.data)]
);
return { importedFolders, isImportedFolderPresentInEnv };
};

View File

@@ -12,6 +12,12 @@ export type TSecretImport = {
updatedAt: string;
};
export type TGetImportedFoldersByEnvDTO = {
projectId: string;
environment: string;
path?: string;
};
export type TImportedSecrets = {
environment: string;
environmentInfo: WorkspaceEnv;
@@ -26,6 +32,13 @@ export type TGetSecretImports = {
path?: string;
};
export type TGetSecretImportsAllEnvs = {
projectId: string;
decryptFileKey: UserWsKeyPair;
path?: string;
environments: string[];
};
export type TGetImportedSecrets = {
projectId: string;
environment: string;
@@ -33,6 +46,12 @@ export type TGetImportedSecrets = {
decryptFileKey: UserWsKeyPair;
};
export type TuseGetImportedFoldersByEnv = {
environments: string[];
projectId: string;
path?: string;
};
export type TCreateSecretImportDTO = {
projectId: string;
environment: string;

View File

@@ -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({

View File

@@ -17,7 +17,7 @@ import {
} from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useGetImportedSecrets,
useGetImportedSecretsSingleEnv,
useGetProjectFolders,
useGetProjectSecrets,
useGetSecretApprovalPolicyOfABoard,
@@ -124,7 +124,7 @@ export const SecretMainPage = () => {
});
// fetch imported secrets to show user the overriden ones
const { data: importedSecrets } = useGetImportedSecrets({
const { data: importedSecrets } = useGetImportedSecretsSingleEnv({
projectId: workspaceId,
environment,
decryptFileKey: decryptFileKey!,

View File

@@ -55,6 +55,7 @@ import {
useCreateSecretV3,
useDeleteSecretV3,
useGetFoldersByEnv,
useGetImportedSecretsAllEnvs,
useGetProjectSecretsAllEnv,
useGetUserWsKey,
useUpdateSecretV3
@@ -131,6 +132,12 @@ export const SecretOverviewPage = () => {
environments: userAvailableEnvs.map(({ slug }) => slug)
});
const { isImportedSecretPresentInEnv } = useGetImportedSecretsAllEnvs({
projectId: workspaceId,
decryptFileKey: latestFileKey!,
environments: userAvailableEnvs.map(({ slug }) => slug)
});
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
@@ -649,6 +656,7 @@ export const SecretOverviewPage = () => {
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}

View File

@@ -15,6 +15,7 @@ export const SecretOverviewFolderRow = ({
folderName,
environments = [],
isFolderPresentInEnv,
onClick
}: Props) => {
return (
@@ -29,6 +30,7 @@ export const SecretOverviewFolderRow = ({
</Td>
{environments.map(({ slug }, i) => {
const isPresent = isFolderPresentInEnv(folderName, slug);
return (
<Td
key={`sec-overview-${slug}-${i + 1}-folder`}
@@ -38,7 +40,10 @@ export const SecretOverviewFolderRow = ({
)}
>
<div className="flex justify-center">
<FontAwesomeIcon icon={isPresent ? faCheck : faXmark} />
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark}
/>
</div>
</Td>
);

View File

@@ -2,6 +2,7 @@ import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
@@ -15,6 +16,7 @@ type Props = {
secretId?: string;
isCreatable?: boolean;
isVisible?: boolean;
isImportedSecret: boolean;
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
@@ -25,6 +27,7 @@ type Props = {
export const SecretEditRow = ({
defaultValue,
isCreatable,
isImportedSecret,
onSecretUpdate,
secretName,
onSecretCreate,
@@ -90,14 +93,25 @@ export const SecretEditRow = ({
<div className="group flex w-full cursor-text items-center space-x-2">
<div className="flex-grow border-r border-r-mineshaft-600 pr-2 pl-1">
<Controller
disabled={isImportedSecret}
control={control}
name="value"
render={({ field }) => (
<SecretInput {...field} value={field.value as string} isVisible={isVisible} />
<SecretInput
{...field}
value={field.value as string}
isVisible={isVisible}
isImport={isImportedSecret}
/>
)}
/>
</div>
<div className="flex w-16 justify-center space-x-3 pl-2 transition-all">
<div
className={twMerge(
"flex w-16 justify-center space-x-3 pl-2 transition-all",
isImportedSecret && "pointer-events-none opacity-0"
)}
>
{isDirty ? (
<>
<ProjectPermissionCan

View File

@@ -4,6 +4,7 @@ import {
faCheck,
faEye,
faEyeSlash,
faFileImport,
faKey,
faXmark
} from "@fortawesome/free-solid-svg-icons";
@@ -26,6 +27,7 @@ type Props = {
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
isImportedSecretPresentInEnv: (name: string, env: string, secretName: string) => boolean;
};
export const SecretOverviewTableRow = ({
@@ -36,6 +38,7 @@ export const SecretOverviewTableRow = ({
onSecretUpdate,
onSecretCreate,
onSecretDelete,
isImportedSecretPresentInEnv,
expandableColWidth
}: Props) => {
const [isFormExpanded, setIsFormExpanded] = useToggle();
@@ -61,6 +64,9 @@ export const SecretOverviewTableRow = ({
</Td>
{environments.map(({ slug }, i) => {
const secret = getSecretByKey(slug, secretKey);
const isSecretImported = isImportedSecretPresentInEnv(secretPath, slug, secretKey);
const isSecretPresent = Boolean(secret);
const isSecretEmpty = secret?.value === "";
return (
@@ -69,16 +75,29 @@ export const SecretOverviewTableRow = ({
className={twMerge(
"py-0 px-0 group-hover:bg-mineshaft-700",
isFormExpanded && "border-t-2 border-mineshaft-500",
isSecretPresent && !isSecretEmpty ? "text-green-600" : "",
isSecretPresent && isSecretEmpty ? "text-yellow" : "",
!isSecretPresent && !isSecretEmpty ? "text-red-600" : ""
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
)}
>
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
<div className="flex justify-center">
{!isSecretEmpty && (
<Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
<Tooltip
center
content={
// eslint-disable-next-line no-nested-ternary
isSecretPresent
? "Present secret"
: isSecretImported
? "Imported secret"
: "Missing secret"
}
>
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={isSecretPresent ? faCheck : isSecretImported ? faFileImport : faXmark}
/>
</Tooltip>
)}
{isSecretEmpty && (
@@ -143,6 +162,12 @@ export const SecretOverviewTableRow = ({
const secret = getSecretByKey(slug, secretKey);
const isCreatable = !secret;
const isImportedSecret = isImportedSecretPresentInEnv(
secretPath,
slug,
secretKey
);
return (
<tr
key={`secret-expanded-${slug}-${secretKey}`}
@@ -163,6 +188,7 @@ export const SecretOverviewTableRow = ({
secretName={secretKey}
defaultValue={secret?.value}
secretId={secret?.id}
isImportedSecret={isImportedSecret}
isCreatable={isCreatable}
onSecretDelete={onSecretDelete}
onSecretCreate={onSecretCreate}

View File

@@ -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>

View File

@@ -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>

View File

@@ -13,7 +13,7 @@ version: 1.0.6
# 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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View 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>

View File

@@ -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

View File

@@ -1,9 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: service-account
type: Opaque
stringData:
serviceAccountAccessKey: <>
serviceAccountPrivateKey: <>
serviceAccountPublicKey: <>

View File

@@ -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{

View File

@@ -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)
}
}

View File

@@ -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.

View File

@@ -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"`

View File

@@ -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"`

View 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
}

View File

@@ -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) {