mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-22 10:12:15 +00:00
Compare commits
55 Commits
infisical/
...
fix/replic
Author | SHA1 | Date | |
---|---|---|---|
|
b10752acb5 | ||
|
dbbd58ffb7 | ||
|
5d2beb3604 | ||
|
ec65e0e29c | ||
|
b819848058 | ||
|
1b0ef540fe | ||
|
4496241002 | ||
|
52e32484ce | ||
|
8b497699d4 | ||
|
be73f62226 | ||
|
102620ff09 | ||
|
994ee88852 | ||
|
770e25b895 | ||
|
fcf3bdb440 | ||
|
89c11b5541 | ||
|
5f764904e2 | ||
|
1a75384dba | ||
|
50f434cd80 | ||
|
d879cfd90c | ||
|
ca1f5eaca3 | ||
|
04086376ea | ||
|
364027a88a | ||
|
ca110d11b0 | ||
|
4e8f404f16 | ||
|
22abb78f48 | ||
|
24f11406e1 | ||
|
d5d67c82b2 | ||
|
35cfcf1f0f | ||
|
2c8cfeb826 | ||
|
70d22f90ec | ||
|
d88a473b47 | ||
|
4f52400887 | ||
|
34eb9f475a | ||
|
902a0b0c56 | ||
|
9e6294786f | ||
|
847c50d2d4 | ||
|
efa043c3d2 | ||
|
7e94791635 | ||
|
eedc5f533e | ||
|
676ebaf3c2 | ||
|
adb3185042 | ||
|
97be31f11e | ||
|
667cceebc0 | ||
|
93445d96b3 | ||
|
6100086338 | ||
|
2d68f9aa16 | ||
|
e694293ebe | ||
|
ef6f5ecc4b | ||
|
56f5249925 | ||
|
df5b3fa8dc | ||
|
035ac0fe8d | ||
|
c12408eb81 | ||
|
13194296c6 | ||
|
c568f40954 | ||
|
28f87b8b27 |
123
.github/workflows/build-docker-image-to-prod.yml
vendored
123
.github/workflows/build-docker-image-to-prod.yml
vendored
@@ -1,123 +0,0 @@
|
||||
name: Release production images (frontend, backend)
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*"
|
||||
- "!infisical/v*.*.*-postgres"
|
||||
|
||||
jobs:
|
||||
backend-image:
|
||||
name: Build backend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- name: 📦 Install dependencies to test all dependencies
|
||||
run: npm ci --only-production
|
||||
working-directory: backend
|
||||
# - name: 🧪 Run tests
|
||||
# run: npm run test:ci
|
||||
# working-directory: backend
|
||||
- name: Save commit hashes for tag
|
||||
id: commit
|
||||
uses: pr-mpt/actions-commit-hash@v2
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
- name: 📦 Build backend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
load: true
|
||||
context: backend
|
||||
tags: infisical/infisical:test
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: ⏻ Spawn backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||
- name: 🧪 Test backend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-backend-test
|
||||
- name: ⏻ Shut down backend container and dependencies
|
||||
run: |
|
||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||
- name: 🏗️ Build backend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
push: true
|
||||
context: backend
|
||||
tags: |
|
||||
infisical/backend:${{ steps.commit.outputs.short }}
|
||||
infisical/backend:latest
|
||||
infisical/backend:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
frontend-image:
|
||||
name: Build frontend image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
- 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 frontend and export to Docker
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
load: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
project: 64mmf0n610
|
||||
context: frontend
|
||||
tags: infisical/frontend:test
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||
- name: ⏻ Spawn frontend container
|
||||
run: |
|
||||
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||
- name: 🧪 Test frontend image
|
||||
run: |
|
||||
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||
- name: ⏻ Shut down frontend container
|
||||
run: |
|
||||
docker stop infisical-frontend-test
|
||||
- name: 🏗️ Build frontend and push
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: 64mmf0n610
|
||||
push: true
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
context: frontend
|
||||
tags: |
|
||||
infisical/frontend:${{ steps.commit.outputs.short }}
|
||||
infisical/frontend:latest
|
||||
infisical/frontend:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
82
.github/workflows/nightly-tag-generation.yml
vendored
Normal file
82
.github/workflows/nightly-tag-generation.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Generate Nightly Tag
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Run daily at midnight UTC
|
||||
workflow_dispatch: # Allow manual triggering for testing
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-nightly-tag:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for tags
|
||||
token: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Generate nightly tag
|
||||
run: |
|
||||
# Get the latest infisical production tag
|
||||
LATEST_STABLE_TAG=$(git tag --list | grep "^v[0-9].*$" | grep -v "nightly" | sort -V | tail -n1)
|
||||
|
||||
if [ -z "$LATEST_STABLE_TAG" ]; then
|
||||
echo "No infisical production tags found, using v0.1.0"
|
||||
LATEST_STABLE_TAG="v0.1.0"
|
||||
fi
|
||||
|
||||
echo "Latest production tag: $LATEST_STABLE_TAG"
|
||||
|
||||
# Get current date in YYYYMMDD format
|
||||
DATE=$(date +%Y%m%d)
|
||||
|
||||
# Base nightly tag name
|
||||
BASE_TAG="${LATEST_STABLE_TAG}-nightly-${DATE}"
|
||||
|
||||
# Check if this exact tag already exists
|
||||
if git tag --list | grep -q "^${BASE_TAG}$"; then
|
||||
echo "Base tag ${BASE_TAG} already exists, finding next increment"
|
||||
|
||||
# Find existing tags for this date and get the highest increment
|
||||
EXISTING_TAGS=$(git tag --list | grep "^${BASE_TAG}" | grep -E '\.[0-9]+$' || true)
|
||||
|
||||
if [ -z "$EXISTING_TAGS" ]; then
|
||||
# No incremental tags exist, create .1
|
||||
NIGHTLY_TAG="${BASE_TAG}.1"
|
||||
else
|
||||
# Find the highest increment
|
||||
HIGHEST_INCREMENT=$(echo "$EXISTING_TAGS" | sed "s|^${BASE_TAG}\.||" | sort -n | tail -n1)
|
||||
NEXT_INCREMENT=$((HIGHEST_INCREMENT + 1))
|
||||
NIGHTLY_TAG="${BASE_TAG}.${NEXT_INCREMENT}"
|
||||
fi
|
||||
else
|
||||
# Base tag doesn't exist, use it
|
||||
NIGHTLY_TAG="$BASE_TAG"
|
||||
fi
|
||||
|
||||
echo "Generated nightly tag: $NIGHTLY_TAG"
|
||||
echo "NIGHTLY_TAG=$NIGHTLY_TAG" >> $GITHUB_ENV
|
||||
echo "LATEST_PRODUCTION_TAG=$LATEST_STABLE_TAG" >> $GITHUB_ENV
|
||||
|
||||
git tag "$NIGHTLY_TAG"
|
||||
git push origin "$NIGHTLY_TAG"
|
||||
echo "✅ Created and pushed nightly tag: $NIGHTLY_TAG"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ env.NIGHTLY_TAG }}
|
||||
name: ${{ env.NIGHTLY_TAG }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
make_latest: false
|
@@ -2,7 +2,9 @@ name: Release standalone docker image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical/v*.*.*-postgres"
|
||||
- "v*.*.*"
|
||||
- "v*.*.*-nightly-*"
|
||||
- "v*.*.*-nightly-*.*"
|
||||
|
||||
jobs:
|
||||
infisical-tests:
|
||||
@@ -17,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -53,7 +55,7 @@ jobs:
|
||||
push: true
|
||||
context: .
|
||||
tags: |
|
||||
infisical/infisical:latest-postgres
|
||||
infisical/infisical:latest
|
||||
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -69,7 +71,7 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version from tag
|
||||
id: extract_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||
run: echo "::set-output name=version::${GITHUB_REF_NAME}"
|
||||
- name: ☁️ Checkout source
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -105,7 +107,7 @@ jobs:
|
||||
push: true
|
||||
context: .
|
||||
tags: |
|
||||
infisical/infisical-fips:latest-postgres
|
||||
infisical/infisical-fips:latest
|
||||
infisical/infisical-fips:${{ steps.commit.outputs.short }}
|
||||
infisical/infisical-fips:${{ steps.extract_version.outputs.version }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
@@ -44,10 +44,7 @@ jobs:
|
||||
|
||||
- name: Generate Helm Chart
|
||||
working-directory: k8-operator
|
||||
run: make helm
|
||||
|
||||
- name: Update Helm Chart Version
|
||||
run: ./k8-operator/scripts/update-version.sh ${{ steps.extract_version.outputs.version }}
|
||||
run: make helm VERSION=${{ steps.extract_version.outputs.version }}
|
||||
|
||||
- name: Debug - Check file changes
|
||||
run: |
|
||||
|
@@ -15,6 +15,7 @@ import { z } from "zod";
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -170,14 +171,29 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
};
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).verifyCredentials(providerInputs.clusterName);
|
||||
return true;
|
||||
try {
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).verifyCredentials(providerInputs.clusterName);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
providerInputs.accessKeyId,
|
||||
providerInputs.secretAccessKey,
|
||||
providerInputs.clusterName,
|
||||
providerInputs.region
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -206,21 +222,37 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement));
|
||||
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).createUser(parsedStatement, providerInputs.clusterName);
|
||||
try {
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).createUser(parsedStatement, providerInputs.clusterName);
|
||||
|
||||
return {
|
||||
entityId: leaseUsername,
|
||||
data: {
|
||||
DB_USERNAME: leaseUsername,
|
||||
DB_PASSWORD: leasePassword
|
||||
}
|
||||
};
|
||||
return {
|
||||
entityId: leaseUsername,
|
||||
data: {
|
||||
DB_USERNAME: leaseUsername,
|
||||
DB_PASSWORD: leasePassword
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
leaseUsername,
|
||||
leasePassword,
|
||||
providerInputs.accessKeyId,
|
||||
providerInputs.secretAccessKey,
|
||||
providerInputs.clusterName
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -229,15 +261,25 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId });
|
||||
const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement));
|
||||
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).deleteUser(parsedStatement);
|
||||
try {
|
||||
await ElastiCacheUserManager(
|
||||
{
|
||||
accessKeyId: providerInputs.accessKeyId,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
},
|
||||
providerInputs.region
|
||||
).deleteUser(parsedStatement);
|
||||
|
||||
return { entityId };
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId, providerInputs.accessKeyId, providerInputs.secretAccessKey, providerInputs.clusterName]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -23,6 +23,7 @@ import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -118,22 +119,39 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs, projectId);
|
||||
const isConnected = await client
|
||||
.send(new GetUserCommand({}))
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
const message = (err as Error)?.message;
|
||||
if (
|
||||
(providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) &&
|
||||
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
|
||||
message.includes("Must specify userName when calling with non-User credentials")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw err;
|
||||
try {
|
||||
const client = await $getClient(providerInputs, projectId);
|
||||
const isConnected = await client
|
||||
.send(new GetUserCommand({}))
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
const message = (err as Error)?.message;
|
||||
if (
|
||||
(providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) &&
|
||||
// assume role will throw an error asking to provider username, but if so this has access in aws correctly
|
||||
message.includes("Must specify userName when calling with non-User credentials")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
return isConnected;
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -162,62 +180,81 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
awsTags.push(...additionalTags);
|
||||
}
|
||||
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn }))
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
try {
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(
|
||||
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
|
||||
@@ -278,8 +315,25 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
)
|
||||
);
|
||||
|
||||
await client.send(new DeleteUserCommand({ UserName: username }));
|
||||
return { entityId: username };
|
||||
try {
|
||||
await client.send(new DeleteUserCommand({ UserName: username }));
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -2,6 +2,7 @@ import axios from "axios";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
|
||||
import { AzureEntraIDSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
@@ -51,45 +52,82 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
try {
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
return data.success;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.clientSecret, providerInputs.applicationId, providerInputs.tenantId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async ({ inputs }: { inputs: unknown }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
|
||||
const password = generatePassword();
|
||||
|
||||
const response = await axios.patch(
|
||||
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||
{
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
try {
|
||||
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
|
||||
if (!data.success) {
|
||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||
}
|
||||
);
|
||||
if (response.status !== 204) {
|
||||
throw new BadRequestError({ message: "Failed to update password" });
|
||||
}
|
||||
|
||||
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||
const response = await axios.patch(
|
||||
`${MSFT_GRAPH_API_URL}/users/${providerInputs.userId}`,
|
||||
{
|
||||
passwordProfile: {
|
||||
forceChangePasswordNextSignIn: false,
|
||||
password
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${data.token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (response.status !== 204) {
|
||||
throw new BadRequestError({ message: "Failed to update password" });
|
||||
}
|
||||
|
||||
return { entityId: providerInputs.userId, data: { email: providerInputs.email, password } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
providerInputs.clientSecret,
|
||||
providerInputs.applicationId,
|
||||
providerInputs.userId,
|
||||
providerInputs.email,
|
||||
password
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
// Creates a new password
|
||||
await create({ inputs });
|
||||
return { entityId };
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
try {
|
||||
// Creates a new password
|
||||
await create({ inputs });
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.clientSecret, providerInputs.applicationId, entityId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
|
||||
|
@@ -3,6 +3,8 @@ import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -71,9 +73,24 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||
await client.shutdown();
|
||||
return isConnected;
|
||||
try {
|
||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||
await client.shutdown();
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
const tokens = [providerInputs.password, providerInputs.username];
|
||||
if (providerInputs.keyspace) {
|
||||
tokens.push(providerInputs.keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -89,23 +106,39 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const { keyspace } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
keyspace
|
||||
});
|
||||
try {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
keyspace
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
const tokens = [username, password];
|
||||
if (keyspace) {
|
||||
tokens.push(keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
await client.shutdown();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -115,14 +148,29 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const username = entityId;
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
try {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
const tokens = [username];
|
||||
if (keyspace) {
|
||||
tokens.push(keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
@@ -130,21 +178,36 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
keyspace,
|
||||
expiration
|
||||
});
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await client.execute(query);
|
||||
try {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
keyspace,
|
||||
expiration
|
||||
});
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const tokens = [entityId];
|
||||
if (keyspace) {
|
||||
tokens.push(keyspace);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
await client.shutdown();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -2,6 +2,8 @@ import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
@@ -63,12 +65,24 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const infoResponse = await connection
|
||||
.info()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
return infoResponse;
|
||||
try {
|
||||
const infoResponse = await connection.info().then(() => true);
|
||||
return infoResponse;
|
||||
} catch (err) {
|
||||
const tokens = [];
|
||||
if (providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey) {
|
||||
tokens.push(providerInputs.auth.apiKey, providerInputs.auth.apiKeyId);
|
||||
} else {
|
||||
tokens.push(providerInputs.auth.username, providerInputs.auth.password);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -79,27 +93,49 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
await connection.security.putUser({
|
||||
username,
|
||||
password,
|
||||
full_name: "Managed by Infisical.com",
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
try {
|
||||
await connection.security.putUser({
|
||||
username,
|
||||
password,
|
||||
full_name: "Managed by Infisical.com",
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
|
||||
await connection.close();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
await connection.close();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password]
|
||||
});
|
||||
await connection.close();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
await connection.security.deleteUser({
|
||||
username: entityId
|
||||
});
|
||||
try {
|
||||
await connection.security.deleteUser({
|
||||
username: entityId
|
||||
});
|
||||
|
||||
await connection.close();
|
||||
return { entityId };
|
||||
await connection.close();
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId]
|
||||
});
|
||||
await connection.close();
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -3,6 +3,7 @@ import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretGcpIamSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -65,8 +66,18 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
await $getToken(providerInputs.serviceAccountEmail, 10);
|
||||
return true;
|
||||
try {
|
||||
await $getToken(providerInputs.serviceAccountEmail, 10);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.serviceAccountEmail]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; expireAt: number }) => {
|
||||
@@ -74,13 +85,23 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const ttl = Math.max(Math.floor(expireAt / 1000) - now, 0);
|
||||
|
||||
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
const token = await $getToken(providerInputs.serviceAccountEmail, ttl);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
|
||||
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
|
||||
return { entityId, data: { SERVICE_ACCOUNT_EMAIL: providerInputs.serviceAccountEmail, TOKEN: token } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.serviceAccountEmail]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (_inputs: unknown, entityId: string) => {
|
||||
@@ -89,10 +110,21 @@ export const GcpIamProvider = (): TDynamicProviderFns => {
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
// To renew a token it must be re-created
|
||||
const data = await create({ inputs, expireAt });
|
||||
try {
|
||||
// To renew a token it must be re-created
|
||||
const data = await create({ inputs, expireAt });
|
||||
|
||||
return { ...data, entityId };
|
||||
return { ...data, entityId };
|
||||
} catch (err) {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.serviceAccountEmail]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
@@ -89,26 +90,46 @@ export const GithubProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
return true;
|
||||
try {
|
||||
await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown }) => {
|
||||
const { inputs } = data;
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
try {
|
||||
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
TOKEN: ghTokenData.token,
|
||||
EXPIRES_AT: ghTokenData.expires_at,
|
||||
PERMISSIONS: ghTokenData.permissions,
|
||||
REPOSITORY_SELECTION: ghTokenData.repository_selection
|
||||
}
|
||||
};
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
TOKEN: ghTokenData.token,
|
||||
EXPIRES_AT: ghTokenData.expires_at,
|
||||
PERMISSIONS: ghTokenData.permissions,
|
||||
REPOSITORY_SELECTION: ghTokenData.repository_selection
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.privateKey, String(providerInputs.appId), String(providerInputs.installationId)]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async () => {
|
||||
|
@@ -2,7 +2,8 @@ import axios, { AxiosError } from "axios";
|
||||
import handlebars from "handlebars";
|
||||
import https from "https";
|
||||
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
@@ -356,8 +357,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to validate connection: ${errorMessage}`
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [providerInputs.clusterToken || ""]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -602,8 +607,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
throw new InternalServerError({
|
||||
message: `Failed to create dynamic secret: ${errorMessage}`
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [providerInputs.clusterToken || ""]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -683,50 +692,65 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
||||
};
|
||||
|
||||
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
|
||||
const rawUrl =
|
||||
providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? GATEWAY_AUTH_DEFAULT_URL
|
||||
: providerInputs.url || "";
|
||||
try {
|
||||
const rawUrl =
|
||||
providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||
? GATEWAY_AUTH_DEFAULT_URL
|
||||
: providerInputs.url || "";
|
||||
|
||||
const url = new URL(rawUrl);
|
||||
const k8sGatewayHost = url.hostname;
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
const url = new URL(rawUrl);
|
||||
const k8sGatewayHost = url.hostname;
|
||||
const k8sPort = url.port ? Number(url.port) : 443;
|
||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||
|
||||
const httpsAgent =
|
||||
providerInputs.ca && providerInputs.sslEnabled
|
||||
? new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
: undefined;
|
||||
const httpsAgent =
|
||||
providerInputs.ca && providerInputs.sslEnabled
|
||||
? new https.Agent({
|
||||
ca: providerInputs.ca,
|
||||
rejectUnauthorized: true
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
if (providerInputs.gatewayId) {
|
||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: true
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
} else {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await $gatewayProxyWrapper(
|
||||
{
|
||||
gatewayId: providerInputs.gatewayId,
|
||||
targetHost: k8sGatewayHost,
|
||||
targetPort: k8sPort,
|
||||
httpsAgent,
|
||||
reviewTokenThroughGateway: false
|
||||
},
|
||||
serviceAccountDynamicCallback
|
||||
);
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||
}
|
||||
} else {
|
||||
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||
} catch (error) {
|
||||
let errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
|
||||
errorMessage = (error.response?.data as { message: string }).message;
|
||||
}
|
||||
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [entityId, providerInputs.clusterToken || ""]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { LdapCredentialType, LdapSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -91,8 +92,18 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
return client.connected;
|
||||
try {
|
||||
const client = await $getClient(providerInputs);
|
||||
return client.connected;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.bindpass, providerInputs.binddn]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const executeLdif = async (client: ldapjs.Client, ldif_file: string) => {
|
||||
@@ -205,11 +216,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||
const dnRegex = new RE2("^dn:\\s*(.+)", "m");
|
||||
const dnMatch = dnRegex.exec(providerInputs.rotationLdif);
|
||||
const username = dnMatch?.[1];
|
||||
if (!username) throw new BadRequestError({ message: "Username not found from Ldif" });
|
||||
const password = generatePassword();
|
||||
|
||||
if (dnMatch) {
|
||||
const username = dnMatch[1];
|
||||
const password = generatePassword();
|
||||
|
||||
const generatedLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rotationLdif });
|
||||
|
||||
try {
|
||||
@@ -217,7 +228,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: (err as Error).message });
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
|
||||
});
|
||||
throw new BadRequestError({ message: sanitizedErrorMessage });
|
||||
}
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
@@ -238,7 +253,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
const rollbackLdif = generateLDIF({ username, password, ldifTemplate: providerInputs.rollbackLdif });
|
||||
await executeLdif(client, rollbackLdif);
|
||||
}
|
||||
throw new BadRequestError({ message: (err as Error).message });
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
|
||||
});
|
||||
throw new BadRequestError({ message: sanitizedErrorMessage });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -262,7 +281,11 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
|
||||
return { entityId: username, data: { DN_ARRAY: dnArray, USERNAME: username, PASSWORD: password } };
|
||||
} catch (err) {
|
||||
throw new BadRequestError({ message: (err as Error).message });
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.binddn, providerInputs.bindpass]
|
||||
});
|
||||
throw new BadRequestError({ message: sanitizedErrorMessage });
|
||||
}
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
@@ -278,7 +301,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string) => {
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
// No renewal necessary
|
||||
return { entityId };
|
||||
};
|
||||
|
@@ -3,6 +3,8 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
|
||||
@@ -49,19 +51,25 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client({
|
||||
method: "GET",
|
||||
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
params: { itemsPerPage: 1 }
|
||||
})
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
try {
|
||||
const isConnected = await client({
|
||||
method: "GET",
|
||||
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
params: { itemsPerPage: 1 }
|
||||
}).then(() => true);
|
||||
return isConnected;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
|
||||
});
|
||||
return isConnected;
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -77,25 +85,39 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
await client({
|
||||
method: "POST",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
data: {
|
||||
roles: providerInputs.roles,
|
||||
scopes: providerInputs.scopes,
|
||||
deleteAfterDate: expiration,
|
||||
username,
|
||||
password,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
try {
|
||||
await client({
|
||||
method: "POST",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
|
||||
data: {
|
||||
roles: providerInputs.roles,
|
||||
scopes: providerInputs.scopes,
|
||||
deleteAfterDate: expiration,
|
||||
username,
|
||||
password,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
});
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [
|
||||
username,
|
||||
password,
|
||||
providerInputs.adminPublicKey,
|
||||
providerInputs.adminPrivateKey,
|
||||
providerInputs.groupId
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -111,15 +133,23 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
throw err;
|
||||
});
|
||||
if (isExisting) {
|
||||
await client({
|
||||
method: "DELETE",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
try {
|
||||
await client({
|
||||
method: "DELETE",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { entityId: username };
|
||||
@@ -132,21 +162,29 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
await client({
|
||||
method: "PATCH",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
|
||||
data: {
|
||||
deleteAfterDate: expiration,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
}).catch((error) => {
|
||||
if ((error as AxiosError).response) {
|
||||
throw new Error(JSON.stringify((error as AxiosError).response?.data));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return { entityId: username };
|
||||
try {
|
||||
await client({
|
||||
method: "PATCH",
|
||||
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
|
||||
data: {
|
||||
deleteAfterDate: expiration,
|
||||
databaseName: "admin",
|
||||
groupId: providerInputs.groupId
|
||||
}
|
||||
});
|
||||
return { entityId: username };
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).response
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error)?.message;
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: [username, providerInputs.adminPublicKey, providerInputs.adminPrivateKey, providerInputs.groupId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -2,6 +2,8 @@ import { MongoClient } from "mongodb";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
@@ -51,13 +53,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const isConnected = await client
|
||||
.db(providerInputs.database)
|
||||
.command({ ping: 1 })
|
||||
.then(() => true);
|
||||
try {
|
||||
const isConnected = await client
|
||||
.db(providerInputs.database)
|
||||
.command({ ping: 1 })
|
||||
.then(() => true);
|
||||
|
||||
await client.close();
|
||||
return isConnected;
|
||||
await client.close();
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.database, providerInputs.host]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -68,16 +81,27 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
try {
|
||||
const db = client.db(providerInputs.database);
|
||||
|
||||
await db.command({
|
||||
createUser: username,
|
||||
pwd: password,
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
await client.close();
|
||||
await db.command({
|
||||
createUser: username,
|
||||
pwd: password,
|
||||
roles: providerInputs.roles
|
||||
});
|
||||
await client.close();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -86,13 +110,24 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const username = entityId;
|
||||
|
||||
const db = client.db(providerInputs.database);
|
||||
await db.command({
|
||||
dropUser: username
|
||||
});
|
||||
await client.close();
|
||||
try {
|
||||
const db = client.db(providerInputs.database);
|
||||
await db.command({
|
||||
dropUser: username
|
||||
});
|
||||
await client.close();
|
||||
|
||||
return { entityId: username };
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -3,6 +3,8 @@ import https from "https";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
@@ -110,11 +112,19 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||
|
||||
return infoResponse;
|
||||
try {
|
||||
const connection = await $getClient(providerInputs);
|
||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||
return infoResponse;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.host]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -125,26 +135,44 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword();
|
||||
|
||||
await createRabbitMqUser({
|
||||
axiosInstance: connection,
|
||||
virtualHost: providerInputs.virtualHost,
|
||||
createUser: {
|
||||
password,
|
||||
username,
|
||||
tags: [...(providerInputs.tags ?? []), "infisical-user"]
|
||||
}
|
||||
});
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
try {
|
||||
await createRabbitMqUser({
|
||||
axiosInstance: connection,
|
||||
virtualHost: providerInputs.virtualHost,
|
||||
createUser: {
|
||||
password,
|
||||
username,
|
||||
tags: [...(providerInputs.tags ?? []), "infisical-user"]
|
||||
}
|
||||
});
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||
|
||||
return { entityId };
|
||||
try {
|
||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||
return { entityId };
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -4,6 +4,7 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -112,14 +113,27 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const connection = await $getClient(providerInputs);
|
||||
|
||||
const pingResponse = await connection
|
||||
.ping()
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
return pingResponse;
|
||||
let connection;
|
||||
try {
|
||||
connection = await $getClient(providerInputs);
|
||||
const pingResponse = await connection.ping().then(() => true);
|
||||
await connection.quit();
|
||||
return pingResponse;
|
||||
} catch (err) {
|
||||
if (connection) await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [
|
||||
providerInputs.password || "",
|
||||
providerInputs.username,
|
||||
providerInputs.host,
|
||||
String(providerInputs.port)
|
||||
]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -144,10 +158,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
|
||||
await executeTransactions(connection, queries);
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
try {
|
||||
await executeTransactions(connection, queries);
|
||||
await connection.quit();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password || "", providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
@@ -159,10 +183,20 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
|
||||
await executeTransactions(connection, queries);
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
try {
|
||||
await executeTransactions(connection, queries);
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password || "", providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
@@ -176,13 +210,23 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
|
||||
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await executeTransactions(connection, queries);
|
||||
try {
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await executeTransactions(connection, queries);
|
||||
}
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await connection.quit();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password || "", providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
await connection.quit();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
|
@@ -4,6 +4,7 @@ import odbc from "odbc";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -67,25 +68,41 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
const client = await $getClient(providerInputs);
|
||||
let masterClient;
|
||||
let client;
|
||||
try {
|
||||
masterClient = await $getClient(providerInputs, true);
|
||||
client = await $getClient(providerInputs);
|
||||
|
||||
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
|
||||
|
||||
if (!resultFromSelectedDatabase.version) {
|
||||
if (!resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection, version query failed"
|
||||
});
|
||||
}
|
||||
|
||||
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection (master), version mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (masterClient) await masterClient.close();
|
||||
if (client) await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.host, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection, version query failed"
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SAP ASE connection (master), version mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const create = async (data: { inputs: unknown; usernameTemplate?: string | null; identity?: { name: string } }) => {
|
||||
@@ -105,16 +122,26 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
// If not done, then the newly created user won't be able to authenticate.
|
||||
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
// If not done, then the newly created user won't be able to authenticate.
|
||||
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
|
||||
}
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
} catch (err) {
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, username: string) => {
|
||||
@@ -140,14 +167,24 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
}
|
||||
}
|
||||
|
||||
for await (const query of queries) {
|
||||
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
|
||||
}
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
return { entityId: username };
|
||||
} catch (err) {
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username, providerInputs.database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
await masterClient.close();
|
||||
await client.close();
|
||||
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (_: unknown, username: string) => {
|
||||
|
@@ -10,6 +10,7 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -83,19 +84,26 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
const testResult = await new Promise<boolean>((resolve, reject) => {
|
||||
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||
if (err) {
|
||||
reject();
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
try {
|
||||
const client = await $getClient(providerInputs);
|
||||
const testResult = await new Promise<boolean>((resolve, reject) => {
|
||||
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return testResult;
|
||||
return testResult;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.host]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -119,18 +127,22 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(true);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) return reject(err);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,18 +154,24 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
const client = await $getClient(providerInputs);
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
}
|
||||
resolve(true);
|
||||
try {
|
||||
for await (const query of queries) {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,16 +192,20 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
await new Promise((resolve, reject) => {
|
||||
client.exec(query, (err: any) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new BadRequestError({
|
||||
message: err.message
|
||||
})
|
||||
);
|
||||
reject(err);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [entityId, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
client.disconnect();
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import snowflake from "snowflake-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
@@ -69,12 +70,10 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs);
|
||||
|
||||
let isValidConnection: boolean;
|
||||
|
||||
let client;
|
||||
try {
|
||||
isValidConnection = await Promise.race([
|
||||
client = await $getClient(providerInputs);
|
||||
const isValidConnection = await Promise.race([
|
||||
client.isValidAsync(),
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 10000);
|
||||
@@ -82,11 +81,18 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
throw new BadRequestError({ message: "Unable to establish connection - verify credentials" });
|
||||
})
|
||||
]);
|
||||
return isValidConnection;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.password, providerInputs.username, providerInputs.accountId, providerInputs.orgId]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
if (client) client.destroy(noop);
|
||||
}
|
||||
|
||||
return isValidConnection;
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
@@ -116,13 +122,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
sqlText: creationStatement,
|
||||
complete(err) {
|
||||
if (err) {
|
||||
return reject(new BadRequestError({ name: "CreateLease", message: err.message }));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error).message,
|
||||
tokens: [username, password, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({ message: `Failed to create lease from provider: ${sanitizedErrorMessage}` });
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
}
|
||||
@@ -143,13 +155,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
sqlText: revokeStatement,
|
||||
complete(err) {
|
||||
if (err) {
|
||||
return reject(new BadRequestError({ name: "RevokeLease", message: err.message }));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error).message,
|
||||
tokens: [username, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({ message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}` });
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
}
|
||||
@@ -175,13 +193,19 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
sqlText: renewStatement,
|
||||
complete(err) {
|
||||
if (err) {
|
||||
return reject(new BadRequestError({ name: "RenewLease", message: err.message }));
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error).message,
|
||||
tokens: [entityId, providerInputs.password, providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({ message: `Failed to renew lease from provider: ${sanitizedErrorMessage}` });
|
||||
} finally {
|
||||
client.destroy(noop);
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ import knex from "knex";
|
||||
import { z } from "zod";
|
||||
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
@@ -212,8 +214,19 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
try {
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
@@ -233,13 +246,13 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const { database } = providerInputs;
|
||||
const username = generateUsername(providerInputs.client, usernameTemplate, identity);
|
||||
|
||||
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
@@ -256,6 +269,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
@@ -283,6 +304,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
@@ -319,6 +348,14 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { authenticator } from "otplib";
|
||||
import { HashAlgorithms } from "otplib/core";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
|
||||
@@ -12,62 +14,84 @@ export const TotpProvider = (): TDynamicProviderFns => {
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const validateConnection = async () => {
|
||||
return true;
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
try {
|
||||
await validateProviderInputs(inputs);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: []
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const create = async (data: { inputs: unknown }) => {
|
||||
const { inputs } = data;
|
||||
try {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
const authenticatorInstance = authenticator.clone();
|
||||
const entityId = alphaNumericNanoId(32);
|
||||
const authenticatorInstance = authenticator.clone();
|
||||
|
||||
let secret: string;
|
||||
let period: number | null | undefined;
|
||||
let digits: number | null | undefined;
|
||||
let algorithm: HashAlgorithms | null | undefined;
|
||||
let secret: string;
|
||||
let period: number | null | undefined;
|
||||
let digits: number | null | undefined;
|
||||
let algorithm: HashAlgorithms | null | undefined;
|
||||
|
||||
if (providerInputs.configType === TotpConfigType.URL) {
|
||||
const urlObj = new URL(providerInputs.url);
|
||||
secret = urlObj.searchParams.get("secret") as string;
|
||||
const periodFromUrl = urlObj.searchParams.get("period");
|
||||
const digitsFromUrl = urlObj.searchParams.get("digits");
|
||||
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
|
||||
if (providerInputs.configType === TotpConfigType.URL) {
|
||||
const urlObj = new URL(providerInputs.url);
|
||||
secret = urlObj.searchParams.get("secret") as string;
|
||||
const periodFromUrl = urlObj.searchParams.get("period");
|
||||
const digitsFromUrl = urlObj.searchParams.get("digits");
|
||||
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
|
||||
|
||||
if (periodFromUrl) {
|
||||
period = +periodFromUrl;
|
||||
if (periodFromUrl) {
|
||||
period = +periodFromUrl;
|
||||
}
|
||||
|
||||
if (digitsFromUrl) {
|
||||
digits = +digitsFromUrl;
|
||||
}
|
||||
|
||||
if (algorithmFromUrl) {
|
||||
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
|
||||
}
|
||||
} else {
|
||||
secret = providerInputs.secret;
|
||||
period = providerInputs.period;
|
||||
digits = providerInputs.digits;
|
||||
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
|
||||
}
|
||||
|
||||
if (digitsFromUrl) {
|
||||
digits = +digitsFromUrl;
|
||||
if (digits) {
|
||||
authenticatorInstance.options = { digits };
|
||||
}
|
||||
|
||||
if (algorithmFromUrl) {
|
||||
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
|
||||
if (algorithm) {
|
||||
authenticatorInstance.options = { algorithm };
|
||||
}
|
||||
} else {
|
||||
secret = providerInputs.secret;
|
||||
period = providerInputs.period;
|
||||
digits = providerInputs.digits;
|
||||
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
|
||||
}
|
||||
|
||||
if (digits) {
|
||||
authenticatorInstance.options = { digits };
|
||||
}
|
||||
if (period) {
|
||||
authenticatorInstance.options = { step: period };
|
||||
}
|
||||
|
||||
if (algorithm) {
|
||||
authenticatorInstance.options = { algorithm };
|
||||
return {
|
||||
entityId,
|
||||
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
|
||||
};
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: []
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
if (period) {
|
||||
authenticatorInstance.options = { step: period };
|
||||
}
|
||||
|
||||
return {
|
||||
entityId,
|
||||
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async (_inputs: unknown, entityId: string) => {
|
||||
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
@@ -275,6 +276,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
|
||||
await client.raw(trimmedQuery);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, providerInputs.username, providerInputs.password]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
if (client) await client.destroy();
|
||||
}
|
||||
@@ -339,6 +348,14 @@ export const VerticaProvider = ({ gatewayService }: TVerticaProviderDTO): TDynam
|
||||
await client.raw(trimmedQuery);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, providerInputs.username, providerInputs.password]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to revoke lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
if (client) await client.destroy();
|
||||
}
|
||||
|
@@ -557,14 +557,13 @@ export const ldapConfigServiceFactory = ({
|
||||
});
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||
hasExchangedPrivateKey: true,
|
||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||
firstName,
|
||||
lastName,
|
||||
|
@@ -404,7 +404,6 @@ export const oidcConfigServiceFactory = ({
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
@@ -417,7 +416,7 @@ export const oidcConfigServiceFactory = ({
|
||||
organizationName: organization.name,
|
||||
organizationId: organization.id,
|
||||
organizationSlug: organization.slug,
|
||||
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||
hasExchangedPrivateKey: true,
|
||||
authMethod: AuthMethod.OIDC,
|
||||
authType: UserAliasType.OIDC,
|
||||
isUserCompleted,
|
||||
|
@@ -411,7 +411,6 @@ export const samlConfigServiceFactory = ({
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
@@ -424,7 +423,7 @@ export const samlConfigServiceFactory = ({
|
||||
organizationId: organization.id,
|
||||
organizationSlug: organization.slug,
|
||||
authMethod: authProvider,
|
||||
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||
hasExchangedPrivateKey: true,
|
||||
authType: UserAliasType.SAML,
|
||||
isUserCompleted,
|
||||
...(relayState
|
||||
|
@@ -19,3 +19,17 @@ export const prefixWithSlash = (str: string) => {
|
||||
const vowelRegex = new RE2(/^[aeiou]/i);
|
||||
|
||||
export const startsWithVowel = (str: string) => vowelRegex.test(str);
|
||||
|
||||
const pickWordsRegex = new RE2(/(\W+)/);
|
||||
export const sanitizeString = (dto: { unsanitizedString: string; tokens: string[] }) => {
|
||||
const words = dto.unsanitizedString.split(pickWordsRegex);
|
||||
|
||||
const redactionSet = new Set(dto.tokens.filter(Boolean));
|
||||
const sanitizedWords = words.map((el) => {
|
||||
if (redactionSet.has(el)) {
|
||||
return "[REDACTED]";
|
||||
}
|
||||
return el;
|
||||
});
|
||||
return sanitizedWords.join("");
|
||||
};
|
||||
|
@@ -849,8 +849,6 @@ export const registerRoutes = async (
|
||||
projectDAL,
|
||||
permissionService,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
projectMembershipDAL
|
||||
});
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { RemindersSchema } from "@app/db/schemas/reminders";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
@@ -628,7 +629,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
tags: SanitizedTagSchema.array().optional()
|
||||
tags: SanitizedTagSchema.array().optional(),
|
||||
reminder: RemindersSchema.extend({
|
||||
recipients: z.string().array().optional()
|
||||
}).nullish()
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
@@ -706,7 +710,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined;
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"] | undefined;
|
||||
let secrets:
|
||||
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
|
||||
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
|
||||
})[]
|
||||
| undefined;
|
||||
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
|
||||
let secretRotations:
|
||||
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
|
||||
@@ -904,7 +912,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||
secrets = (
|
||||
const rawSecrets = (
|
||||
await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@@ -925,6 +933,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
includeMetadataInSearch: true
|
||||
})
|
||||
).secrets;
|
||||
|
||||
const reminders = await server.services.reminder.getRemindersForDashboard(
|
||||
rawSecrets.map((secret) => secret.id)
|
||||
);
|
||||
|
||||
secrets = rawSecrets.map((secret) => ({
|
||||
...secret,
|
||||
reminder: reminders[secret.id] ?? null
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -824,7 +824,6 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||
const isUserCompleted = user.isAccepted;
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
@@ -835,7 +834,7 @@ export const authLoginServiceFactory = ({
|
||||
isEmailVerified: user.isEmailVerified,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
|
||||
hasExchangedPrivateKey: true,
|
||||
authMethod,
|
||||
isUserCompleted,
|
||||
...(callbackPort
|
||||
@@ -880,8 +879,7 @@ export const authLoginServiceFactory = ({
|
||||
const userEnc =
|
||||
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
||||
|
||||
if (!userEnc?.serverEncryptedPrivateKey)
|
||||
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
|
||||
if (!userEnc) throw new BadRequestError({ message: "User encryption not found" });
|
||||
|
||||
const token = await generateUserTokens({
|
||||
user: { ...userEnc, id: userEnc.userId },
|
||||
|
@@ -6,8 +6,6 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
@@ -20,8 +18,6 @@ type TOrgAdminServiceFactoryDep = {
|
||||
TProjectMembershipDALFactory,
|
||||
"findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
@@ -32,8 +28,6 @@ export const orgAdminServiceFactory = ({
|
||||
permissionService,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}: TOrgAdminServiceFactoryDep) => {
|
||||
@@ -119,28 +113,6 @@ export const orgAdminServiceFactory = ({
|
||||
return { isExistingMember: true, membership: projectMembership };
|
||||
}
|
||||
|
||||
// missing membership thus add admin back as admin to project
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
if (!ghostUser) {
|
||||
throw new NotFoundError({
|
||||
message: `Project owner of project with ID '${projectId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new NotFoundError({
|
||||
message: `Project owner's latest key of project with ID '${projectId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new NotFoundError({
|
||||
message: `Project bot for project with ID '${projectId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
|
||||
const newProjectMembership = await projectMembershipDAL.create(
|
||||
{
|
||||
|
@@ -124,10 +124,35 @@ export const reminderDALFactory = (db: TDbClient) => {
|
||||
return reminders[0] || null;
|
||||
};
|
||||
|
||||
const findSecretReminders = async (secretIds: string[], tx?: Knex) => {
|
||||
const rawReminders = await (tx || db)(TableName.Reminder)
|
||||
.whereIn(`${TableName.Reminder}.secretId`, secretIds)
|
||||
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
|
||||
.select(selectAllTableCols(TableName.Reminder))
|
||||
.select(db.ref("userId").withSchema(TableName.ReminderRecipient));
|
||||
const reminders = sqlNestRelationships({
|
||||
data: rawReminders,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
_id: el.id,
|
||||
...RemindersSchema.parse(el)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "userId",
|
||||
label: "recipients" as const,
|
||||
mapper: ({ userId }) => userId
|
||||
}
|
||||
]
|
||||
});
|
||||
return reminders;
|
||||
};
|
||||
|
||||
return {
|
||||
...reminderOrm,
|
||||
findSecretDailyReminders,
|
||||
findUpcomingReminders,
|
||||
findSecretReminder
|
||||
findSecretReminder,
|
||||
findSecretReminders
|
||||
};
|
||||
};
|
||||
|
@@ -372,6 +372,21 @@ export const reminderServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getRemindersForDashboard: TReminderServiceFactory["getRemindersForDashboard"] = async (secretIds) => {
|
||||
// scott we don't need to check permissions/secret existence because these are the
|
||||
// secrets from the dashboard that have already gone through these checks
|
||||
|
||||
const reminders = await reminderDAL.findSecretReminders(secretIds);
|
||||
|
||||
const reminderMap: Record<string, (typeof reminders)[number]> = {};
|
||||
|
||||
reminders.forEach((reminder) => {
|
||||
if (reminder.secretId) reminderMap[reminder.secretId] = reminder;
|
||||
});
|
||||
|
||||
return reminderMap;
|
||||
};
|
||||
|
||||
return {
|
||||
createReminder,
|
||||
getReminder,
|
||||
@@ -379,6 +394,7 @@ export const reminderServiceFactory = ({
|
||||
deleteReminder,
|
||||
deleteReminderBySecretId,
|
||||
batchCreateReminders,
|
||||
createReminderInternal
|
||||
createReminderInternal,
|
||||
getRemindersForDashboard
|
||||
};
|
||||
};
|
||||
|
@@ -103,4 +103,6 @@ export interface TReminderServiceFactory {
|
||||
id: string;
|
||||
created: boolean;
|
||||
}>;
|
||||
|
||||
getRemindersForDashboard: (secretIds: string[]) => Promise<Record<string, TReminder & { recipients: string[] }>>;
|
||||
}
|
||||
|
@@ -41,8 +41,6 @@
|
||||
"group": "Platform Reference",
|
||||
"pages": [
|
||||
"documentation/platform/organization",
|
||||
"documentation/platform/event-subscriptions",
|
||||
"documentation/platform/folder",
|
||||
{
|
||||
"group": "Projects",
|
||||
"pages": [
|
||||
@@ -145,6 +143,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"documentation/platform/event-subscriptions",
|
||||
{
|
||||
"group": "Workflow Integrations",
|
||||
"pages": [
|
||||
@@ -303,6 +302,7 @@
|
||||
},
|
||||
"self-hosting/guides/upgrading-infisical",
|
||||
"self-hosting/configuration/envars",
|
||||
"self-hosting/guides/releases",
|
||||
"self-hosting/configuration/requirements",
|
||||
{
|
||||
"group": "Guides",
|
||||
@@ -388,6 +388,32 @@
|
||||
"group": "Secrets Management",
|
||||
"pages": [
|
||||
"documentation/platform/secrets-mgmt/overview",
|
||||
{
|
||||
"group": "Concepts",
|
||||
"pages": [
|
||||
"documentation/platform/secrets-mgmt/concepts/secrets-mgmt",
|
||||
"documentation/platform/secrets-mgmt/concepts/access-control",
|
||||
"documentation/platform/secrets-mgmt/concepts/secrets-delivery",
|
||||
"documentation/platform/secrets-mgmt/concepts/secrets-rotation",
|
||||
"documentation/platform/secrets-mgmt/concepts/dynamic-secrets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Guides",
|
||||
"pages": [
|
||||
"documentation/guides/introduction",
|
||||
"documentation/guides/local-development",
|
||||
"documentation/guides/node",
|
||||
"documentation/guides/python",
|
||||
"documentation/guides/nextjs-vercel",
|
||||
"documentation/guides/microsoft-power-apps"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Product Reference",
|
||||
"pages": [
|
||||
"documentation/platform/secrets-mgmt/project",
|
||||
"documentation/platform/folder",
|
||||
{
|
||||
@@ -432,17 +458,6 @@
|
||||
"documentation/platform/dynamic-secrets/kubernetes",
|
||||
"documentation/platform/dynamic-secrets/vertica"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Guides",
|
||||
"pages": [
|
||||
"documentation/guides/introduction",
|
||||
"documentation/guides/local-development",
|
||||
"documentation/guides/node",
|
||||
"documentation/guides/python",
|
||||
"documentation/guides/nextjs-vercel",
|
||||
"documentation/guides/microsoft-power-apps"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -466,7 +481,7 @@
|
||||
"group": "Agent",
|
||||
"pages": [
|
||||
"integrations/platforms/infisical-agent",
|
||||
"integrations/platforms/docker-swarm-with-agent",
|
||||
"integrations/platforms/docker-swarm-with-agent",
|
||||
"integrations/platforms/ecs-with-agent"
|
||||
]
|
||||
},
|
||||
@@ -635,14 +650,21 @@
|
||||
"item": "Secrets Scanning",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Secret Scanning",
|
||||
"group": "Secrets Scanning",
|
||||
"pages": [
|
||||
"documentation/platform/secret-scanning/overview"
|
||||
"documentation/platform/secret-scanning/overview",
|
||||
{
|
||||
"group": "Concepts",
|
||||
"pages": [
|
||||
"documentation/platform/secret-scanning/concepts/secret-scanning"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Datasources",
|
||||
"group": "Product Reference",
|
||||
"pages": [
|
||||
"documentation/platform/secret-scanning/usage",
|
||||
"documentation/platform/secret-scanning/bitbucket",
|
||||
"documentation/platform/secret-scanning/github",
|
||||
"documentation/platform/secret-scanning/gitlab"
|
||||
@@ -682,6 +704,18 @@
|
||||
"group": "Infisical SSH",
|
||||
"pages": [
|
||||
"documentation/platform/ssh/overview",
|
||||
{
|
||||
"group": "Concepts",
|
||||
"pages": [
|
||||
"documentation/platform/ssh/concepts/ssh-certificates"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Platform Reference",
|
||||
"pages": [
|
||||
"documentation/platform/ssh/usage",
|
||||
"documentation/platform/ssh/host-groups"
|
||||
]
|
||||
}
|
||||
|
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: "Secrets Scanning"
|
||||
description: "Learn what is secret scanning and why it matters for building secure systems."
|
||||
---
|
||||
|
||||
## What is Secret Scanning?
|
||||
|
||||
_Secret scanning_ is the process of monitoring code and related systems for exposed secrets — such as API keys, database credentials, and authentication tokens — that may have been accidentally committed or leaked.
|
||||
|
||||
As teams grow and development accelerates, it becomes easy for secrets to slip into version control, CI/CD pipelines, or shared files. Left undetected, secrets can fall into the wrong hands and give attackers direct access to production systems, third-party services, or internal APIs.
|
||||
|
||||
A secret scanning solution helps teams proactively identify and respond to these risks before they result in compromise. Rather than relying on manual review, secret scanning automates detection through pattern matching, entropy analysis, and contextual rules that surface secrets across your infrastructure and repositories.
|
||||
|
||||
## Secret Scanning in Infisical
|
||||
|
||||
Infisical Secret Scanning continuously monitors your source code and connected systems for exposed credentials. It integrates with platforms like [GitHub](/documentation/platform/secret-scanning/github), [GitLab](/documentation/platform/secret-scanning/gitlab), and [Bitbucket](/documentation/platform/secret-scanning/bitbucket) to scan codebases in real-time, detecting leaks as they happen and notifying administrators when action is needed.
|
||||
|
||||
Findings are surfaced with detailed context — including file location, commit metadata, and rule match — and can be tracked through their lifecycle using status labels like `Resolved`, `False Positive`, or `Ignored`. Teams can configure rules, exclusions, and thresholds to reduce noise and tailor detection to their environment.
|
||||
|
||||
In addition to real-time monitoring, Infisical supports both full repository scans and lightweight diff scans, as well as local pre-commit scanning via the [Infisical CLI](/cli/commands/scan). This allows teams to prevent secret leaks before they ever reach production.
|
@@ -1,230 +1,17 @@
|
||||
---
|
||||
title: "Secret Scanning"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Scan and prevent secret leaks in your code repositories"
|
||||
description: "Learn how to detect and respond to exposed secrets in code."
|
||||
---
|
||||
|
||||
## Introduction
|
||||
Infisical Secret Scanning helps teams detect leaked credentials — such as API keys, database passwords, and tokens — across source code and developer systems. It allows organizations to proactively catch exposed secrets before they can be exploited, and respond quickly when incidents occur.
|
||||
|
||||
Monitor and detect exposed secrets across your data sources, including code repositories, with Infisical Secret Scanning.
|
||||
Secret Scanning works across both cloud-connected repositories and local developer environments. It integrates with data sources like [GitHub](/documentation/platform/secret-scanning/github), [GitLab](/documentation/platform/secret-scanning/gitlab), and [Bitbucket](/documentation/platform/secret-scanning/bitbucket) to monitor repositories for exposed secrets in real time, and provides a CLI ([`infisical scan`](/cli/commands/scan)) for scanning local directories, Git history, or CI pipelines before changes are pushed.
|
||||
|
||||
For additional security, we recommend using our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to check for exposed secrets before pushing your code changes.
|
||||
Core capabilities include:
|
||||
|
||||
<Note>
|
||||
Secret Scanning is a paid feature.
|
||||
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact team@infisical.com to purchase an enterprise license to use it.
|
||||
</Note>
|
||||
|
||||
## How Secret Scanning Works
|
||||
|
||||
Secret Scanning consists of several components that enable you to quickly respond to secret leaks:
|
||||
|
||||
- **Scanner Engine**: The core component that analyzes your code and detects potential secrets using pattern matching and entropy analysis
|
||||
- **Real-time Monitoring**: Provides continuous surveillance of your repositories for immediate detection of exposed secrets
|
||||
- **Alert System**: Notifies organization admins via email when secrets are detected
|
||||
- **Risk Management**: Allows tracking and managing detected secrets with different status options
|
||||
- **Data Sources**: Integrates with various data sources and version control systems
|
||||
- **Customizable Rules**: Supports ignore patterns and custom configurations to reduce false positives
|
||||
|
||||
These components work together to provide comprehensive secret detection and incident response capabilities.
|
||||
|
||||
### Data Sources
|
||||
|
||||
Data sources are configured integrations with external platforms, such as a GitHub organization or a GitLab group, that establish secure connections for scanning purposes using [App Connections](/integrations/app-connections/overview).
|
||||
|
||||
A data source acts as a secure intermediary between the external system and the scanner engine. It manages a collection of scannable resources (such as repositories) and handles the authentication and communication required for scanning operations.
|
||||
|
||||

|
||||
|
||||
### Resources
|
||||
|
||||
Resources are the atomic, scannable units, such as a repository, that can be monitored for secret exposure. Resources are added automatically when a data source is scanned and updated when scanning events are triggered, such as when a user pushes changes to GitHub.
|
||||
|
||||
Each resource maintains its own scanning history and status, allowing for granular monitoring and management of secret scanning across your organization.
|
||||
|
||||

|
||||
|
||||
### Scans
|
||||
|
||||
Scans can be initiated in two ways:
|
||||
|
||||
1. **Full Scan** - Manually triggered scan that comprehensively checks either all resources associated with a data source or a single selected resource.
|
||||
|
||||
2. **Diff Scan** - Automatically executed when **Auto-Scan** is enabled on a data source. This scan type specifically focuses on updates to existing resources.
|
||||
|
||||
All scan activities can be monitored in real-time through the Infisical UI, which displays:
|
||||
- Current scan status
|
||||
- Timestamp of the scan
|
||||
- Resource(s) being scanned
|
||||
- Detection results (whether any secrets were found)
|
||||
|
||||

|
||||
|
||||
### Findings
|
||||
|
||||
Findings are automatically generated when secret leaks are detected during scanning operations. Each finding contains comprehensive information including:
|
||||
- The specific scanning rule that identified the leak
|
||||
- File location and line number where the secret was found
|
||||
- Resource-specific details (e.g., commit hash and author for Git repositories)
|
||||
|
||||
Findings are initially marked as **Unresolved** and can be updated to one of the following statuses with additional remarks:
|
||||
- **Resolved** - The issue has been addressed
|
||||
- **False Positive** - The detection was incorrect
|
||||
- **Ignore** - The finding can be safely disregarded
|
||||
|
||||
These status options help teams effectively track and manage the lifecycle of detected secret leaks.
|
||||
|
||||

|
||||
|
||||
### Configuration
|
||||
|
||||
You can configure custom scanning rules and exceptions by updating your project's scanning configuration via the UI or API.
|
||||
|
||||
The configuration options allow you to:
|
||||
- Define custom scanning patterns and rules
|
||||
- Set up ignore patterns to reduce false positives
|
||||
- Specify file path exclusions
|
||||
- Configure entropy thresholds for secret detection
|
||||
- Add allowlists for known safe patterns
|
||||
|
||||
For detailed configuration options, expand the example configuration below.
|
||||
|
||||
<Accordion title="Example Configuration">
|
||||
```toml
|
||||
# Title for the configuration file
|
||||
title = "Some title"
|
||||
|
||||
|
||||
# This configuration is the foundation that can be expanded. If there are any overlapping rules
|
||||
# between this base and the expanded configuration, the rules in this base will take priority.
|
||||
# Another aspect of extending configurations is the ability to link multiple files, up to a depth of 2.
|
||||
# "Allowlist" arrays get appended and may have repeated elements.
|
||||
# "useDefault" and "path" cannot be used simultaneously. Please choose one.
|
||||
[extend]
|
||||
# useDefault will extend the base configuration with the default config:
|
||||
# https://raw.githubusercontent.com/Infisical/infisical/main/cli/config/infisical-scan.toml
|
||||
useDefault = true
|
||||
# or you can supply a path to a configuration. Path is relative to where infisical cli
|
||||
# was invoked, not the location of the base config.
|
||||
path = "common_config.toml"
|
||||
|
||||
# An array of tables that contain information that define instructions
|
||||
# on how to detect secrets
|
||||
[[rules]]
|
||||
|
||||
# Unique identifier for this rule
|
||||
id = "some-identifier-for-rule"
|
||||
|
||||
# Short human readable description of the rule.
|
||||
description = "awesome rule 1"
|
||||
|
||||
# Golang regular expression used to detect secrets. Note Golang's regex engine
|
||||
# does not support lookaheads.
|
||||
regex = '''one-go-style-regex-for-this-rule'''
|
||||
|
||||
# Golang regular expression used to match paths. This can be used as a standalone rule or it can be used
|
||||
# in conjunction with a valid `regex` entry.
|
||||
path = '''a-file-path-regex'''
|
||||
|
||||
# Array of strings used for metadata and reporting purposes.
|
||||
tags = ["tag","another tag"]
|
||||
|
||||
# A regex match may have many groups, this allows you to specify the group that should be used as (which group the secret is contained in)
|
||||
# its entropy checked if `entropy` is set.
|
||||
secretGroup = 3
|
||||
|
||||
# Float representing the minimum shannon entropy a regex group must have to be considered a secret.
|
||||
# Shannon entropy measures how random a data is. Since secrets are usually composed of many random characters, they typically have high entropy
|
||||
entropy = 3.5
|
||||
|
||||
# Keywords are used for pre-regex check filtering.
|
||||
# If rule has keywords but the text fragment being scanned doesn't have at least one of it's keywords, it will be skipped for processing further.
|
||||
# Ideally these values should either be part of the identifier or unique strings specific to the rule's regex
|
||||
# (introduced in v8.6.0)
|
||||
keywords = [
|
||||
"auth",
|
||||
"password",
|
||||
"token",
|
||||
]
|
||||
|
||||
# You can include an allowlist table for a single rule to reduce false positives or ignore commits
|
||||
# with known/rotated secrets
|
||||
[rules.allowlist]
|
||||
description = "ignore commit A"
|
||||
commits = [ "commit-A", "commit-B"]
|
||||
paths = [
|
||||
'''go\.mod''',
|
||||
'''go\.sum'''
|
||||
]
|
||||
# note: (rule) regexTarget defaults to check the _Secret_ in the finding.
|
||||
# if regexTarget is not specified then _Secret_ will be used.
|
||||
# Acceptable values for regexTarget are "match" and "line"
|
||||
regexTarget = "match"
|
||||
regexes = [
|
||||
'''process''',
|
||||
'''getenv''',
|
||||
]
|
||||
# note: stopwords targets the extracted secret, not the entire regex match
|
||||
# if the extracted secret is found in the stopwords list, the finding will be skipped (i.e not included in report)
|
||||
stopwords = [
|
||||
'''client''',
|
||||
'''endpoint''',
|
||||
]
|
||||
|
||||
|
||||
# This is a global allowlist which has a higher order of precedence than rule-specific allowlists.
|
||||
# If a commit listed in the `commits` field below is encountered then that commit will be skipped and no
|
||||
# secrets will be detected for said commit. The same logic applies for regexes and paths.
|
||||
[allowlist]
|
||||
description = "global allow list"
|
||||
commits = [ "commit-A", "commit-B", "commit-C"]
|
||||
paths = [
|
||||
'''gitleaks\.toml''',
|
||||
'''(.*?)(jpg|gif|doc)'''
|
||||
]
|
||||
|
||||
# note: (global) regexTarget defaults to check the _Secret_ in the finding.
|
||||
# if regexTarget is not specified then _Secret_ will be used.
|
||||
# Acceptable values for regexTarget are "match" and "line"
|
||||
regexTarget = "match"
|
||||
|
||||
regexes = [
|
||||
'''219-09-9999''',
|
||||
'''078-05-1120''',
|
||||
'''(9[0-9]{2}|666)-\d{2}-\d{4}''',
|
||||
]
|
||||
# note: stopwords targets the extracted secret, not the entire regex match
|
||||
# if the extracted secret is found in the stopwords list, the finding will be skipped (i.e not included in report)
|
||||
stopwords = [
|
||||
'''client''',
|
||||
'''endpoint''',
|
||||
]
|
||||
```
|
||||
</Accordion>
|
||||
|
||||

|
||||
|
||||
## Ignoring Known Secrets
|
||||
If you're intentionally committing a test secret that the secret scanner might flag, you can instruct Infisical to overlook that secret with the methods listed below.
|
||||
|
||||
### infisical-scan:ignore
|
||||
|
||||
To ignore a secret contained in line of code, simply add `infisical-scan:ignore ` at the end of the line as comment in the given programming.
|
||||
|
||||
```js example.js
|
||||
function helloWorld() {
|
||||
console.log("8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"); // infisical-scan:ignore
|
||||
}
|
||||
```
|
||||
|
||||
### .infisicalignore
|
||||
An alternative method to exclude specific findings involves creating a .infisicalignore file at your repository's root.
|
||||
You can then add the fingerprints of the findings you wish to exclude. The [Infisical scan](/cli/scanning-overview) report provides a unique Fingerprint for each secret found.
|
||||
By incorporating these Fingerprints into the .infisicalignore file, Infisical will skip the corresponding secret findings in subsequent scans.
|
||||
|
||||
```.ignore .infisicalignore
|
||||
bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147
|
||||
bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5
|
||||
1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148
|
||||
```
|
||||
- Integrated Scanning Across Environments: Monitor secrets in real time across connected repositories like GitHub, GitLab, and Bitbucket, or scan locally using the infisical scan CLI.
|
||||
- Detection Engine: Identify potential secrets using pattern matching, entropy analysis, and custom rules tailored to your codebase and workflows.
|
||||
- Flexible Scan Modes: Run full scans manually or configure automatic diff scans triggered by new commits. CLI scans support Git history, file directories, or staged changes in CI pipelines.
|
||||
- Findings and Lifecycle Management: Track detected secrets with context like file path, commit hash, and scanning rule. Findings can be resolved, ignored, or marked as false positives — with full visibility into scan results over time.
|
||||
- Custom Configuration and Noise Reduction: Fine-tune scanning behavior with custom patterns, ignore rules (infisical-scan:ignore, .infisicalignore), entropy thresholds, and excluded paths to reduce false positives.
|
||||
|
236
docs/documentation/platform/secret-scanning/usage.mdx
Normal file
236
docs/documentation/platform/secret-scanning/usage.mdx
Normal file
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: "Usage"
|
||||
description: "Learn what is secret scanning and why it matters for building secure systems."
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Monitor and detect exposed secrets across your data sources, including code repositories, with Infisical Secret Scanning.
|
||||
|
||||
For additional security, we recommend using our [CLI Secret Scanner](/cli/scanning-overview#automatically-scan-changes-before-you-commit) to check for exposed secrets before pushing your code changes.
|
||||
|
||||
<Note>
|
||||
Secret Scanning is a paid feature. If you're using Infisical Cloud, then it is
|
||||
available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||
then you should contact team@infisical.com to purchase an enterprise license
|
||||
to use it.
|
||||
</Note>
|
||||
|
||||
## How Secret Scanning Works
|
||||
|
||||
Secret Scanning consists of several components that enable you to quickly respond to secret leaks:
|
||||
|
||||
- **Scanner Engine**: The core component that analyzes your code and detects potential secrets using pattern matching and entropy analysis
|
||||
- **Real-time Monitoring**: Provides continuous surveillance of your repositories for immediate detection of exposed secrets
|
||||
- **Alert System**: Notifies organization admins via email when secrets are detected
|
||||
- **Risk Management**: Allows tracking and managing detected secrets with different status options
|
||||
- **Data Sources**: Integrates with various data sources and version control systems
|
||||
- **Customizable Rules**: Supports ignore patterns and custom configurations to reduce false positives
|
||||
|
||||
These components work together to provide comprehensive secret detection and incident response capabilities.
|
||||
|
||||
### Data Sources
|
||||
|
||||
Data sources are configured integrations with external platforms, such as a GitHub organization or a GitLab group, that establish secure connections for scanning purposes using [App Connections](/integrations/app-connections/overview).
|
||||
|
||||
A data source acts as a secure intermediary between the external system and the scanner engine. It manages a collection of scannable resources (such as repositories) and handles the authentication and communication required for scanning operations.
|
||||
|
||||

|
||||
|
||||
### Resources
|
||||
|
||||
Resources are the atomic, scannable units, such as a repository, that can be monitored for secret exposure. Resources are added automatically when a data source is scanned and updated when scanning events are triggered, such as when a user pushes changes to GitHub.
|
||||
|
||||
Each resource maintains its own scanning history and status, allowing for granular monitoring and management of secret scanning across your organization.
|
||||
|
||||

|
||||
|
||||
### Scans
|
||||
|
||||
Scans can be initiated in two ways:
|
||||
|
||||
1. **Full Scan** - Manually triggered scan that comprehensively checks either all resources associated with a data source or a single selected resource.
|
||||
|
||||
2. **Diff Scan** - Automatically executed when **Auto-Scan** is enabled on a data source. This scan type specifically focuses on updates to existing resources.
|
||||
|
||||
All scan activities can be monitored in real-time through the Infisical UI, which displays:
|
||||
|
||||
- Current scan status
|
||||
- Timestamp of the scan
|
||||
- Resource(s) being scanned
|
||||
- Detection results (whether any secrets were found)
|
||||
|
||||

|
||||
|
||||
### Findings
|
||||
|
||||
Findings are automatically generated when secret leaks are detected during scanning operations. Each finding contains comprehensive information including:
|
||||
|
||||
- The specific scanning rule that identified the leak
|
||||
- File location and line number where the secret was found
|
||||
- Resource-specific details (e.g., commit hash and author for Git repositories)
|
||||
|
||||
Findings are initially marked as **Unresolved** and can be updated to one of the following statuses with additional remarks:
|
||||
|
||||
- **Resolved** - The issue has been addressed
|
||||
- **False Positive** - The detection was incorrect
|
||||
- **Ignore** - The finding can be safely disregarded
|
||||
|
||||
These status options help teams effectively track and manage the lifecycle of detected secret leaks.
|
||||
|
||||

|
||||
|
||||
### Configuration
|
||||
|
||||
You can configure custom scanning rules and exceptions by updating your project's scanning configuration via the UI or API.
|
||||
|
||||
The configuration options allow you to:
|
||||
|
||||
- Define custom scanning patterns and rules
|
||||
- Set up ignore patterns to reduce false positives
|
||||
- Specify file path exclusions
|
||||
- Configure entropy thresholds for secret detection
|
||||
- Add allowlists for known safe patterns
|
||||
|
||||
For detailed configuration options, expand the example configuration below.
|
||||
|
||||
<Accordion title="Example Configuration">
|
||||
```toml
|
||||
# Title for the configuration file
|
||||
title = "Some title"
|
||||
|
||||
# This configuration is the foundation that can be expanded. If there are any overlapping rules
|
||||
# between this base and the expanded configuration, the rules in this base will take priority.
|
||||
# Another aspect of extending configurations is the ability to link multiple files, up to a depth of 2.
|
||||
# "Allowlist" arrays get appended and may have repeated elements.
|
||||
# "useDefault" and "path" cannot be used simultaneously. Please choose one.
|
||||
[extend]
|
||||
# useDefault will extend the base configuration with the default config:
|
||||
# https://raw.githubusercontent.com/Infisical/infisical/main/cli/config/infisical-scan.toml
|
||||
useDefault = true
|
||||
# or you can supply a path to a configuration. Path is relative to where infisical cli
|
||||
# was invoked, not the location of the base config.
|
||||
path = "common_config.toml"
|
||||
|
||||
# An array of tables that contain information that define instructions
|
||||
# on how to detect secrets
|
||||
[[rules]]
|
||||
|
||||
# Unique identifier for this rule
|
||||
id = "some-identifier-for-rule"
|
||||
|
||||
# Short human readable description of the rule.
|
||||
description = "awesome rule 1"
|
||||
|
||||
# Golang regular expression used to detect secrets. Note Golang's regex engine
|
||||
# does not support lookaheads.
|
||||
regex = '''one-go-style-regex-for-this-rule'''
|
||||
|
||||
# Golang regular expression used to match paths. This can be used as a standalone rule or it can be used
|
||||
# in conjunction with a valid `regex` entry.
|
||||
path = '''a-file-path-regex'''
|
||||
|
||||
# Array of strings used for metadata and reporting purposes.
|
||||
tags = ["tag","another tag"]
|
||||
|
||||
# A regex match may have many groups, this allows you to specify the group that should be used as (which group the secret is contained in)
|
||||
# its entropy checked if `entropy` is set.
|
||||
secretGroup = 3
|
||||
|
||||
# Float representing the minimum shannon entropy a regex group must have to be considered a secret.
|
||||
# Shannon entropy measures how random a data is. Since secrets are usually composed of many random characters, they typically have high entropy
|
||||
entropy = 3.5
|
||||
|
||||
# Keywords are used for pre-regex check filtering.
|
||||
# If rule has keywords but the text fragment being scanned doesn't have at least one of it's keywords, it will be skipped for processing further.
|
||||
# Ideally these values should either be part of the identifier or unique strings specific to the rule's regex
|
||||
# (introduced in v8.6.0)
|
||||
keywords = [
|
||||
"auth",
|
||||
"password",
|
||||
"token",
|
||||
]
|
||||
|
||||
# You can include an allowlist table for a single rule to reduce false positives or ignore commits
|
||||
# with known/rotated secrets
|
||||
[rules.allowlist]
|
||||
description = "ignore commit A"
|
||||
commits = [ "commit-A", "commit-B"]
|
||||
paths = [
|
||||
'''go\.mod''',
|
||||
'''go\.sum'''
|
||||
]
|
||||
# note: (rule) regexTarget defaults to check the _Secret_ in the finding.
|
||||
# if regexTarget is not specified then _Secret_ will be used.
|
||||
# Acceptable values for regexTarget are "match" and "line"
|
||||
regexTarget = "match"
|
||||
regexes = [
|
||||
'''process''',
|
||||
'''getenv''',
|
||||
]
|
||||
# note: stopwords targets the extracted secret, not the entire regex match
|
||||
# if the extracted secret is found in the stopwords list, the finding will be skipped (i.e not included in report)
|
||||
stopwords = [
|
||||
'''client''',
|
||||
'''endpoint''',
|
||||
]
|
||||
|
||||
|
||||
# This is a global allowlist which has a higher order of precedence than rule-specific allowlists.
|
||||
# If a commit listed in the `commits` field below is encountered then that commit will be skipped and no
|
||||
# secrets will be detected for said commit. The same logic applies for regexes and paths.
|
||||
[allowlist]
|
||||
description = "global allow list"
|
||||
commits = [ "commit-A", "commit-B", "commit-C"]
|
||||
paths = [
|
||||
'''gitleaks\.toml''',
|
||||
'''(.*?)(jpg|gif|doc)'''
|
||||
]
|
||||
|
||||
# note: (global) regexTarget defaults to check the _Secret_ in the finding.
|
||||
# if regexTarget is not specified then _Secret_ will be used.
|
||||
# Acceptable values for regexTarget are "match" and "line"
|
||||
regexTarget = "match"
|
||||
|
||||
regexes = [
|
||||
'''219-09-9999''',
|
||||
'''078-05-1120''',
|
||||
'''(9[0-9]{2}|666)-\d{2}-\d{4}''',
|
||||
]
|
||||
# note: stopwords targets the extracted secret, not the entire regex match
|
||||
# if the extracted secret is found in the stopwords list, the finding will be skipped (i.e not included in report)
|
||||
stopwords = [
|
||||
'''client''',
|
||||
'''endpoint''',
|
||||
]
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||

|
||||
|
||||
## Ignoring Known Secrets
|
||||
|
||||
If you're intentionally committing a test secret that the secret scanner might flag, you can instruct Infisical to overlook that secret with the methods listed below.
|
||||
|
||||
### infisical-scan:ignore
|
||||
|
||||
To ignore a secret contained in line of code, simply add `infisical-scan:ignore ` at the end of the line as comment in the given programming.
|
||||
|
||||
```js example.js
|
||||
function helloWorld() {
|
||||
console.log("8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ"); // infisical-scan:ignore
|
||||
}
|
||||
```
|
||||
|
||||
### .infisicalignore
|
||||
|
||||
An alternative method to exclude specific findings involves creating a .infisicalignore file at your repository's root.
|
||||
You can then add the fingerprints of the findings you wish to exclude. The [Infisical scan](/cli/scanning-overview) report provides a unique Fingerprint for each secret found.
|
||||
By incorporating these Fingerprints into the .infisicalignore file, Infisical will skip the corresponding secret findings in subsequent scans.
|
||||
|
||||
```.ignore .infisicalignore
|
||||
bea0ff6e05a4de73a5db625d4ae181a015b50855:frontend/components/utilities/attemptLogin.js:stripe-access-token:147
|
||||
bea0ff6e05a4de73a5db625d4ae181a015b50855:backend/src/json/integrations.json:generic-api-key:5
|
||||
1961b92340e5d2613acae528b886c842427ce5d0:frontend/components/utilities/attemptLogin.js:stripe-access-token:148
|
||||
```
|
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "Scoping Secrets"
|
||||
description: "Learn how access to secrets is controlled in Infisical."
|
||||
---
|
||||
|
||||
## Secret Hierarchy
|
||||
|
||||
Every secret in Infisical is scoped to an environment and a path.
|
||||
|
||||
- An environment separates where secrets are used, such as `development`, `staging`, or `production`.
|
||||
- A path is an (optional) namespace within an environment that groups related secrets such as `/postgres`, `/redis`, or per-service paths like `/service-a`.
|
||||
|
||||
This structure makes it easy to organize secrets by team, service, or environment, and sets the foundation for controlling who can access what.
|
||||
|
||||
## Access Control
|
||||
|
||||
Access control determines who (or what) can access a secret and under what conditions. Without clear policies, even securely stored secrets can be misused or exposed.
|
||||
|
||||
To control access to secrets, you configure role-based permissions at the project level. These permissions determine which environments and paths a user or machine identity with that role can access. For example, an engineer might have a role that allows them to read secrets in the `development` environment but not those in the `production` environment.
|
||||
|
||||
This model follows the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) such that each user or machine identity has access only to the secrets it needs — and nothing more.
|
||||
|
||||
## Advanced Capabilities
|
||||
|
||||
Beyond basic role assignments, Infisical includes additional access control mechanisms for more advanced use cases:
|
||||
|
||||
- Access approvals: Users can request access to specific environments or paths. Access can be temporary and reviewed before it is granted, reducing long-term exposure.
|
||||
|
||||
- Secret change approvals: Updates to sensitive secrets can require approval before taking effect. This adds control in environments where unreviewed changes pose risk.
|
||||
|
||||
- Attribute-based access control (ABAC): Permissions can be matched against metadata on a user or machine identity — such as team, service, or environment — enabling dynamic access rules without manual role changes.
|
||||
|
||||
All access and approval actions are logged, so it’s always possible to trace who accessed what, when, and under what conditions.
|
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: "Dynamic Secrets"
|
||||
description: "Learn what dynamic secrets are, why they're useful, and how Infisical enables them."
|
||||
---
|
||||
|
||||
## What is a Dynamic Secret?
|
||||
|
||||
A _dynamic secret_ is a time-bound credential generated on demand for a specific user or system. Unlike _static secrets_, which are created and stored ahead of time, or rotated credentials, which are periodically replaced, dynamic secrets don’t exist until they’re requested — and automatically expire shortly after use.
|
||||
|
||||
Each secret is unique to the identity that requested it, reducing the risk of reuse, long-term exposure, or accidental leaks. Because they are short-lived and tightly scoped, dynamic secrets are well suited for high-security environments, automated systems, and ephemeral workloads where access needs to be both temporary and auditable.
|
||||
|
||||
By limiting the lifespan and visibility of credentials, dynamic secrets offer a strong alternative to managing long-lived secrets manually.
|
||||
|
||||
## Dynamic Secrets in Infisical
|
||||
|
||||
Infisical generates dynamic secrets in real time when a user or machine identity requests access. Each secret is uniquely scoped to the requesting identity, valid just-in-time only for a limited duration, and automatically revoked after it expires.
|
||||
|
||||
Because they are short-lived and identity-specific, dynamic secrets reduce the risk of credential reuse, accidental exposure, or long-term persistence across environments.
|
||||
|
||||
When supported for a given integration, using dynamic secrets is strongly recommended. Infisical currently supports dynamic secret templates for commonly used systems including [PostgreSQL](/documentation/platform/dynamic-secrets/postgresql), [MySQL](/documentation/platform/dynamic-secrets/mysql), [Microsoft SQL Server](/documentation/platform/dynamic-secrets/mssql), [MongoDB Atlas](/documentation/platform/dynamic-secrets/mongo-db), [Redis](/documentation/platform/dynamic-secrets/redis), [AWS IAM](/documentation/platform/dynamic-secrets/aws-iam), [GCP IAM](/documentation/platform/dynamic-secrets/gcp-iam), [Azure Entra ID](/documentation/platform/dynamic-secrets/azure-entra-id), and more.
|
||||
|
||||
To learn more, refer to the [dynamic secrets documentation](/documentation/platform/dynamic-secrets/overview).
|
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "Delivering Secrets"
|
||||
description: "Learn how to get secrets out of Infisical and into the systems, applications, and environments that need them."
|
||||
---
|
||||
|
||||
Once secrets are stored and scoped in Infisical, the next step is delivering them securely to the systems and applications that need them.
|
||||
|
||||
Infisical supports many delivery methods to match a wide range of environments — from [local development](/documentation/platform/secrets-mgmt/concepts/secrets-delivery#local-development%2C-scripts%2C-and-one-off-tasks) to [Kubernetes workloads](/documentation/platform/secrets-mgmt/concepts/secrets-delivery#kubernetes-workloads), [CI/CD pipelines](/documentation/platform/secrets-mgmt/concepts/secrets-delivery#ci%2Fcd-pipelines), [infrastructure-as-code tools](/documentation/platform/secrets-mgmt/concepts/secrets-delivery#infrastructure-as-code-and-automation-tools), and more.
|
||||
|
||||
The table below provides a quick overview of which delivery method may be suitable to use based on your environment and how secrets are consumed:
|
||||
|
||||
| Use Case / Environment | Recommended Method(s) | Consumes Secrets As | Notes |
|
||||
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------ |
|
||||
| Local development or scripting | [Infisical CLI](/cli/overview) | Environment variables | Easiest way to inject secrets during local dev or debugging |
|
||||
| Application code fetching at runtime | [SDKs](/sdks/overview), [HTTP API](/api-reference/overview/introduction) | In-memory / API call | Full control in app code; supports dynamic or ephemeral fetching |
|
||||
| VMs, containers, or CI jobs needing preloaded secrets | [Infisical Agent](/integrations/platforms/infisical-agent) | Env vars or files | Good for non-interactive workloads; avoids inline secret fetch |
|
||||
| GitHub Actions | [Secrets Action](https://github.com/Infisical/secrets-action), [Secret Syncs](/integrations/secret-syncs/github) | Env vars or files | Use Action for dynamic fetch; use Syncs to preload into GitHub |
|
||||
| GitLab CI, Jenkins, other CI | [Infisical CLI](/cli/overview), [Infisical Agent](/integrations/platforms/infisical-agent), [Secret Syncs](/integrations/secret-syncs/gitlab) | Env vars or files | Choose based on timing — fetch at runtime vs. pre-populate ahead |
|
||||
| Kubernetes (declarative secrets) | [Kubernetes Operator](/integrations/platforms/kubernetes/overview) | Kubernetes Secrets | Syncs from Infisical into native Kubernetes Secrets |
|
||||
| Kubernetes (ESO-based workflows) | [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical/) | Kubernetes Secrets | Reuses existing ESO setup; Infisical acts as a provider |
|
||||
| Kubernetes (file-based, no K8s secrets) | [Kubernetes Agent Injector](/integrations/platforms/kubernetes-injector) | Mounted files | Injects secrets via init container into volume at pod startup |
|
||||
| Kubernetes (file-based, with rotation) | [Kubernetes CSI Provider](/integrations/platforms/kubernetes-csi) | Mounted files | Uses CSI driver to mount secrets as files with automatic rotation |
|
||||
| Image builds (VMs or containers) | [Packer Plugin](/integrations/frameworks/packer) | Env vars or files | Inject secrets at image build time |
|
||||
| Ansible automation | [Ansible Collection](/integrations/platforms/ansible) | Variables | Runtime secret fetching in playbooks using lookup plugin |
|
||||
| Terraform / Pulumi | [Terraform Provider](/integrations/frameworks/terraform), [Pulumi](/integrations/frameworks/pulumi) | Inputs / ephemeral resources | Use ephemeral for security; avoids storing secrets in state |
|
||||
| Third-party platforms (GitHub, AWS, etc.) | [Secret Syncs](/integrations/secret-syncs/overview) | Preloaded secrets | Push secrets to platforms that can't fetch directly from Infisical |
|
||||
|
||||
From here, you can explore the delivery method that best matches your environment:
|
||||
|
||||
## Local Development, Scripts, and One-Off Tasks
|
||||
|
||||
For local development, one-off scripts, or basic automation, the [Infisical CLI](/cli/overview) is a quick and flexible option.
|
||||
|
||||
Instead of using a `.env` file, you can use [`infisical run`](/cli/commands/run) to inject secrets as environment variables directly into your development process. This provides a cleaner and more secure workflow. You can also use [`infisical secrets`](/cli/commands/secrets#infisical-secrets) to perform CRUD operations on secrets from the command line, which works well for debugging, local tooling, and lightweight scripting.
|
||||
|
||||
To learn more, refer to the [CLI quickstart](cli/usage).
|
||||
|
||||
## Applications and Services
|
||||
|
||||
When secrets need to be accessed directly from within application code, Infisical provides SDKs for [Node.js](https://github.com/Infisical/node-sdk-v2), [Python](https://github.com/Infisical/python-sdk-official), [Go](/sdks/languages/go), [Java](https://github.com/Infisical/java-sdk), [.NET](https://github.com/Infisical/infisical-dotnet-sdk), [C++](https://github.com/Infisical/infisical-cpp-sdk), [Rust](https://github.com/Infisical/rust-sdk), and [Ruby](/sdks/languages/ruby).
|
||||
|
||||
These SDKs let services fetch secrets at runtime or startup. For unsupported languages or if you prefer direct integration, you can use the fully documented [HTTP API](/api-reference/overview/introduction) to fetch secrets within your application logic.
|
||||
|
||||
This approach gives you fine-grained control but also requires managing authentication and caching.
|
||||
|
||||
## Virtual Machines(VMs), Containers, and CI Environments
|
||||
|
||||
For systems that shouldn’t fetch secrets themselves — such as production VMs, Docker containers, or CI jobs — the [Infisical Agent](/integrations/platforms/infisical-agent) can be used to sync secrets into the local environment on their behalf.
|
||||
|
||||
The agent runs as a lightweight background process and supports injecting secrets into files, environment variables, or application config formats. It's especially useful for non-interactive workloads that expect secrets to already exist at runtime.
|
||||
|
||||
You can run the agent as a standalone binary, as a Docker sidecar, or embedded in automation scripts. It works well in environments like:
|
||||
|
||||
- VMs that need secrets provisioned at startup.
|
||||
- [Docker Swarm](/integrations/platforms/docker-swarm-with-agent) services using shared volumes.
|
||||
- [ECS tasks](/integrations/platforms/ecs-with-agent) using EFS for shared secret delivery.
|
||||
|
||||
## CI/CD Pipelines
|
||||
|
||||
For CI/CD pipelines, the right method depends on the platform.
|
||||
|
||||
- On GitHub Actions, the [Infisical Secrets Action](https://github.com/Infisical/secrets-action) provides a native integration that injects secrets as environment variables or `.env` files during workflows. It supports authentication via [AWS IAM](/documentation/platform/identities/aws-auth), [OIDC](/documentation/platform/identities/oidc-auth/github), or [Universal Auth](/documentation/platform/identities/universal-auth) using a Machine Identity.
|
||||
- On other CI platforms like GitLab CI, CircleCI, or Jenkins, the CLI or Agent may be used depending on how secrets are consumed — whether at runtime or during setup.
|
||||
|
||||
Some CI/CD systems also support [Secret Syncs](/integrations/secret-syncs/overview) as an alternative. Instead of fetching secrets dynamically, you can configure Infisical to forward secrets into [GitHub Actions](/integrations/secret-syncs/github), [GitLab CI](/integrations/secret-syncs/gitlab), and similar platforms ahead of time — allowing them to be used as native environment secrets during jobs.
|
||||
|
||||
## Kubernetes Workloads
|
||||
|
||||
Infisical supports multiple options for delivering secrets into Kubernetes, each designed to match different operational models and consumption patterns:
|
||||
|
||||
- [Infisical Kubernetes Operator](/integrations/platforms/kubernetes/overview): A set of CRDs that sync secrets from Infisical into Kubernetes Secrets, push secrets from Kubernetes back to Infisical, and manage dynamic secrets with automatic leases. Best suited for teams using declarative workflows and wanting to treat secrets as part of infrastructure code.
|
||||
|
||||
- [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical/): Enables syncing Infisical secrets into Kubernetes by defining ExternalSecret resources. Ideal if your team already uses ESO as a centralized way to fetch secrets from multiple providers.
|
||||
|
||||
- [Infisical Agent Injector](/integrations/platforms/kubernetes-injector): A Kubernetes mutating admission webhook that injects an init container into your pods. The injected container syncs secrets from Infisical into a shared volume at startup, making them available as files. Useful for workloads that expect file-based secrets and where avoiding Kubernetes Secrets entirely is preferred.
|
||||
|
||||
- [Infisical CSI Provider](/integrations/platforms/kubernetes-csi): Integrates with the Kubernetes Secrets Store CSI Driver to mount secrets as files in pods. Supports automatic rotation and fine-grained control over how secrets are structured and updated. Suitable for environments that require file-based secret delivery with rotation, without persisting Kubernetes Secrets.
|
||||
|
||||
These methods provide secure, declarative integrations with Kubernetes-native workflows.
|
||||
|
||||
## Forwarding to Third-Party Platforms
|
||||
|
||||
In some cases, secrets must be delivered into systems that can’t fetch them from Infisical directly.
|
||||
|
||||
[Secret Syncs](/integrations/secret-syncs/overview) allow you to forward secrets to platforms such as [GitHub](/integrations/secret-syncs/github), [GitLab](/integrations/secret-syncs/gitlab), [AWS Secrets Manager](/integrations/secret-syncs/aws-secrets-manager), [Vercel](/integrations/secret-syncs/vercel), and more.
|
||||
|
||||
This is useful when external systems require secrets to be available ahead of time or expect them in a specific location.
|
||||
|
||||
## Infrastructure-as-Code and Automation Tools
|
||||
|
||||
Infisical integrates with common IaC and automation tools to help you securely inject secrets into your infrastructure provisioning workflows:
|
||||
|
||||
- [Terraform](/integrations/frameworks/terraform): Use the official Infisical Terraform provider to fetch secrets either as ephemeral resources (never written to state files) or as traditional data sources. Ideal for managing cloud infrastructure while keeping secrets secure and version-safe.
|
||||
- [Pulumi](/integrations/frameworks/pulumi): Integrate Infisical into Pulumi projects using the Terraform Bridge, allowing you to fetch and manage secrets in TypeScript, Go, Python, or C# — without changing your existing workflows.
|
||||
- [Ansible](/integrations/platforms/ansible): Retrieve secrets from Infisical at runtime using the official Ansible Collection and lookup plugin. Works well for dynamic configuration during playbook execution.
|
||||
- [Packer](/integrations/frameworks/packer): Inject secrets into VM or container images at build time using the Infisical Packer Plugin — useful for provisioning base images that require secure configuration values.
|
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "Secrets Management"
|
||||
description: "Learn what is secrets management and why it matters for building secure systems."
|
||||
---
|
||||
|
||||
## What is Secret?
|
||||
|
||||
A _secret_ is a confidential value used by an application such as database credential, API key, or other configuration.
|
||||
|
||||
In most cases, secrets allow applications to access systems and control how they behave across the development cycle — for example, an application might use a database password stored in an environment variable like `DB_PASSWORD` to connect to production data. These secrets must be kept secure to protect infrastructure and data.
|
||||
|
||||
## What is Secrets Management?
|
||||
|
||||
As infrastructure scales and systems become more distributed, [secrets sprawl](https://infisical.com/blog/what-is-secret-sprawl). Without consistent security practices, secrets get hardcoded in source code, exposed in environment variables, left unrotated for long periods, and scattered across systems without clear visibility into who can access them.
|
||||
|
||||
To solve secret sprawl, organizations rely on [secrets management](https://infisical.com/blog/what-is-secrets-management): the practice of centralizing secrets and managing them through well-defined workflows. This includes secure storage, fine-grained access controls, automatic rotation, audit logging, and support for dynamic, short-lived credentials.
|
||||
|
||||
A consistent approach makes it easier to keep secrets safe, reduce risk, and operate reliably across environments.
|
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "Secrets Rotation"
|
||||
description: "Learn what secrets rotation is, why it matters, and how Infisical enables it."
|
||||
---
|
||||
|
||||
## What is Secrets Rotation?
|
||||
|
||||
Secrets rotation is the process of regularly replacing credentials like API keys, database passwords, and tokens to reduce the risk of long-term exposure. Even if a secret is compromised, frequent rotation limits how long it can be used.
|
||||
|
||||
Without rotation, secrets often go unchanged for months or years — hardcoded in codebases, embedded in CI pipelines, or shared across environments. Over time, this increases the risk of leaks, misuse, and operational blind spots.
|
||||
|
||||
## Secrets Rotation in Infisical
|
||||
|
||||
Infisical automates rotation using a rolling lifecycle model where new credentials are issued on a fixed schedule with previous ones remaining temporarily valid to give systems time to update without disruption. Each secret moves through three phases: active, inactive, and eventually revoked. This ensures that applications continue to function smoothly throughout the rotation process.
|
||||
|
||||
When rotation is applicable for a given secret type, using it is strongly recommended. Infisical supports configuring automatic rotation for a growing set of use cases including [PostgreSQL](/documentation/platform/secret-rotation/postgres-credentials), [MySQL](/documentation/platform/secret-rotation/mysql-credentials), [Microsoft SQL Server](/documentation/platform/secret-rotation/mssql-credentials), [OracleDB](/documentation/platform/secret-rotation/oracledb-credentials), [LDAP](/documentation/platform/secret-rotation/ldap-password), [AWS IAM users](/documentation/platform/secret-rotation/aws-iam-user-secret), [Azure](/documentation/platform/secret-rotation/azure-client-secret) and [Okta](/documentation/platform/secret-rotation/okta-client-secret) client secrets, and more.
|
||||
|
||||
To learn more, refer to the [secrets rotation documentation](/documentation/platform/secret-rotation/overview).
|
@@ -12,6 +12,6 @@ Core capabilities include:
|
||||
|
||||
- Secret Stores: Secure, versioned storage scoped by [project](/documentation/platform/secrets-mgmt/project), [environment](/documentation/platform/secrets-mgmt/project#project-environments), and [path](/documentation/platform/folder).
|
||||
- [Access Control](/documentation/platform/access-controls/overview): Fine-grained, identity-aware permissions for users and machines
|
||||
- Secret Delivery: Access secrets via [CLI](/cli/overview), [SDKs](/sdks/overview) (Go, Node.js, Python, etc.), [HTTP API](/api-reference/overview/introduction), [agents](/integrations/platforms/infisical-agent), [Kubernetes Operator](/integrations/platforms/kubernetes/overview), [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical), and more.
|
||||
- Lifecycle Automation: Automate [secret rotation](/documentation/platform/secret-rotation/overview), generate [dynamic secrets](/documentation/platform/dynamic-secrets/overview), and enforce [approval-based workflows](/documentation/platform/pr-workflows).
|
||||
- [Secret Delivery](/documentation/platform/secrets-mgmt/concepts/secrets-delivery): Access secrets via [CLI](/cli/overview), [SDKs](/sdks/overview) (Go, Node.js, Python, etc.), [HTTP API](/api-reference/overview/introduction), [agents](/integrations/platforms/infisical-agent), [Kubernetes Operator](/integrations/platforms/kubernetes/overview), [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical), and more.
|
||||
- Lifecycle Automation: Automate [secret rotation](/documentation/platform/secrets-mgmt/concepts/secrets-rotation), generate [dynamic secrets](/documentation/platform/secrets-mgmt/concepts/dynamic-secrets), and enforce [approval-based workflows](/documentation/platform/pr-workflows).
|
||||
- [Secrets Syncs](/integrations/secret-syncs/overview): Push secrets to external services like [GitHub](/integrations/secret-syncs/github), [GitLab](/integrations/secret-syncs/gitlab), [AWS Secrets Manager](/integrations/secret-syncs/aws-secrets-manager), [Vercel](/integrations/secret-syncs/vercel), and more.
|
||||
|
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: "SSH Certificates"
|
||||
description: "Learn what SSH certificates are, why they're useful, and how they enable secure, scalable infrastructure access."
|
||||
---
|
||||
|
||||
SSH access is ubiquitous — It's how engineers, scripts, and platforms across the world remotely administer Linux systems. That said, as teams and systems grow, managing access with static SSH keys becomes brittle and issues like key sprawl, unclear boundaries, and poor revocation hygiene start to emerge.
|
||||
|
||||
_SSH certificates_ offer an alternative approach to securing and managing access at scale.
|
||||
|
||||
## What is an SSH Certificate?
|
||||
|
||||
An _SSH certificate_ is a short-lived, signed credential that proves a user or host’s identity. Unlike static SSH keys, which are distributed and managed manually, SSH certificates rely on a centralized certificate authority (CA) to vouch for identities.
|
||||
There are two types of SSH certificates:
|
||||
|
||||
- User certificates: Issued to users to authenticate with remote hosts
|
||||
- Host certificates: Issued to hosts so clients can verify they're trusted
|
||||
|
||||
Because certificates are time-bound and centrally managed, they’re easier to audit, revoke, and scale across infrastructure.
|
||||
|
||||
## SSH with Infisical
|
||||
|
||||
Infisical SSH gives you a secure, scalable way to manage infrastructure access using SSH certificates — without the overhead of running your own certificate authority, wiring trust across hosts, or building issuance workflows from scratch.
|
||||
|
||||
It replaces long-lived SSH keys with short-lived, identity-bound certificates and handles all the moving parts for you: operating CAs, configuring trust between users and hosts, and issuing certificates on demand. With Infisical SSH, you can register a host with [`infisical ssh add-host`](/docs/cli/commands/ssh#infisical-ssh-add-host), then connect with [`infisical ssh connect`](/docs/cli/commands/ssh#infisical-ssh-connect) — that’s all it takes.
|
||||
|
||||
The result is centralized, auditable SSH access that’s easy to use and built to scale with your infrastructure.
|
@@ -1,201 +1,15 @@
|
||||
---
|
||||
title: "Overview"
|
||||
title: "Infisical SSH"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Learn how to securely provision user SSH access to your infrastructure using SSH certificates."
|
||||
description: "Learn how to manage secure, short-lived SSH access to infrastructure using certificates."
|
||||
---
|
||||
|
||||
## Concept
|
||||
Infisical SSH provides a secure, certificate-based solution for managing SSH access to infrastructure.
|
||||
It replaces long-lived SSH keys with short-lived, identity-bound certificates to reduce key sprawl, simplify access control, and improve auditability.
|
||||
|
||||
Infisical SSH can be configured to provide users on your team short-lived, secure SSH access to infrastructure. Under the hood, it uses SSH certificates
|
||||
and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management,
|
||||
unauthorized access, and SSH key sprawl.
|
||||
Core capabilities include:
|
||||
|
||||
The following entities are important to understand when configuring and using Infisical SSH:
|
||||
|
||||
- Administrator: An individual on your team who is responsible for configuring Infisical SSH.
|
||||
- Users: Other individuals that gain access to remote hosts through Infisical SSH.
|
||||
- Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH.
|
||||
|
||||
## Workflow
|
||||
|
||||
The typical workflow for using Infisical SSH consists of the following steps:
|
||||
|
||||
1. The administrator registers a remote host with Infisical using the Infisical CLI via the `infisical ssh add-host` command.
|
||||
2. The administrator configures Infisical SSH to grant users access to the remote host.
|
||||
3. User(s) access the remote host using the Infisical CLI via the `infisical ssh connect` command.
|
||||
|
||||
## Admin Guide for Configuring Infisical SSH
|
||||
|
||||
In the following steps, we explore how to configure Infisical SSH to control and streamline your team's SSH access to infrastructure. As part of this guide,
|
||||
we will register a remote host with Infisical through a [machine identity](/documentation/platform/identities/machine-identities) and configure Infisical to grant user(s) access to the remote host.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an Infisical SSH project">
|
||||
Start by creating a new Infisical SSH project in Infisical.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Create a machine identity for bootstrapping Infisical SSH">
|
||||
2.1. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
|
||||
|
||||
By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the identity to authenticate with Infisical
|
||||
as part of registering a remote host in step 3.
|
||||
|
||||
<Note>
|
||||
You may use other authentication methods as suitable (e.g. [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), [GCP Auth](/documentation/platform/identities/gcp-auth), etc.) as part of the machine identity configuration but, to keep this example simple, we will be using Universal Auth.
|
||||
</Note>
|
||||
|
||||
2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the **SSH Host Bootstrapper** role.
|
||||
|
||||
This role grants the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical
|
||||
and establish the necessary configuration on it.
|
||||
|
||||
<Tip>
|
||||
If you plan to use a custom role to bootstrap SSH hosts, ensure the role has the **Create** and **Issue Host Certificates** on the **SSH Host** resource.
|
||||
</Tip>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Configure the remote host">
|
||||
3.1. Follow the instructions [here](/cli/overview) to install the Infisical CLI onto the remote host.
|
||||
|
||||
3.2. Run the commands below to register the remote host with Infisical.
|
||||
|
||||
Use the **Client ID** and **Client Secret** from the machine identity you created in step 2.1 as part of the `infisical login` command
|
||||
to obtain an access token and save it as an environment variable.
|
||||
|
||||
```bash
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain)
|
||||
```
|
||||
|
||||
Next, use the `infisical ssh add-host` command to register the remote host with Infisical. As part of this command, input the ID of the Infisical SSH project you created in step 1 for the `--projectId` flag and the hostname of the remote host for the `--hostname` flag.
|
||||
|
||||
```bash
|
||||
sudo infisical ssh add-host --projectId=<project-id> --hostname=<hostname> --token="$INFISICAL_TOKEN" --write-user-ca-to-file --write-host-cert-to-file --configure-sshd
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Note that if you're self-hosting Infisical, you can use the `--domain` flag on the `infisical login` command to specify the domain of your Infisical instance.
|
||||
|
||||
For more information on the `infisical ssh add-host` command, please refer to the Infisical CLI [documentation](/cli/overview).
|
||||
</Tip>
|
||||
|
||||
If successful, you should see output similar to the following:
|
||||
|
||||
```bash
|
||||
✅ Successfully registered host: <hostname>
|
||||
📁 Wrote User CA public key to: /etc/ssh/infisical_user_ca.pub
|
||||
📁 Wrote host certificate to: /etc/ssh/ssh_host_ed25519_key-cert.pub
|
||||
📄 Updated sshd_config entries
|
||||
```
|
||||
|
||||
Finally, use the following command to reload the SSH daemon on the remote host to apply the changes:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload sshd
|
||||
```
|
||||
|
||||
<Note>
|
||||
The command may differ depending on the host. For older versions of Ubuntu/Debian/CentOS, you may need to use `sudo service ssh reload` instead;
|
||||
for Alpine or minimal systems, `/etc/init.d/sshd reload`.
|
||||
</Note>
|
||||
|
||||
Back in Infisical, you should now see the remote host you just registered in the Infisical SSH project you created in step 1 under the **Hosts** tab.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Grant users access to the remote host">
|
||||
4.1. Add the user(s) you wish to grant access to the remote host to the Infisical SSH project under Access Control > Users.
|
||||
|
||||

|
||||
|
||||
4.2. On the registered host in the **Hosts** tab, click **Edit SSH Host** and add a login mapping for the user(s) you added in step 4.1.
|
||||
|
||||
The login mapping dictates what user(s) will be allowed access to the remote host and under a specific login user; in the allowed principals,
|
||||
you should select user(s) part of the Infisical SSH project that will be allowed to login to the remote host as the login user.
|
||||
|
||||
For instance, if you add a mapping with the login user `ec2-user` to some users John and Alice in Infisical, then they will be allowed to login to the remote host as `ec2-user` which is a system user that
|
||||
exists on the remote host.
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
Note that you should configure authorized principals files for each login user you add to the remote host.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## User Guide for SSHing to a Host
|
||||
|
||||
Once Infisical SSH is configured by an administrator, users can SSH to the remote host using the Infisical CLI.
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the Infisical CLI">
|
||||
Follow the instructions [here](/cli/overview) to install the Infisical CLI onto your local machine.
|
||||
</Step>
|
||||
<Step title="Connect to the remote host">
|
||||
The `infisical ssh connect` command can be used in either interactive or non-interactive mode to connect to a remote host.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Interactive Mode">
|
||||
In interactive mode, you'll first need to authenticate with Infisical by running:
|
||||
|
||||
```bash
|
||||
infisical login
|
||||
```
|
||||
|
||||
Then simply run:
|
||||
|
||||
```bash
|
||||
infisical ssh connect
|
||||
```
|
||||
|
||||
You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by
|
||||
the administrator.
|
||||
|
||||
```bash
|
||||
Use the arrow keys to navigate: ↓ ↑ → ←
|
||||
? Select an SSH Host:
|
||||
▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com
|
||||
```
|
||||
|
||||
After selecting a host, you'll be prompted to select a login user from a list of allowed login users:
|
||||
|
||||
```bash
|
||||
? Select Login User:
|
||||
▸ ec2-user
|
||||
```
|
||||
|
||||
If successful, you should be able to SSH to the remote host.
|
||||
|
||||
```bash
|
||||
✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com
|
||||
✔ ec2-user
|
||||
✔ SSH credentials successfully added to agent
|
||||
Connecting to ec2-user@ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com...
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Non-Interactive Mode">
|
||||
For CI/CD pipelines or automation scenarios, you can use the non-interactive mode with an Infisical token:
|
||||
|
||||
```bash
|
||||
infisical ssh connect \
|
||||
--hostname ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com \
|
||||
--login-user ec2-user \
|
||||
--out-file-path ~/.ssh/id_rsa-cert.pub \
|
||||
--token <your-infisical-token>
|
||||
```
|
||||
|
||||
This will:
|
||||
- Connect to the specified hostname
|
||||
- Use the specified login user
|
||||
- Write the SSH credentials to the specified path instead of adding them to the SSH agent
|
||||
- Authenticate using the provided Infisical token
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
- Certificate Authority (CA): Managed CA infrastructure for issuing short-lived SSH certificates.
|
||||
- Centralized Access Management: View all registered hosts, see who has access to each, and control permissions from a single interface.
|
||||
- Easy Host Registration & Connection: Quickly register hosts and connect using short-lived certificates without manual key distribution.
|
||||
- Audit Logging: Detailed records of certificate issuance and SSH access activity.
|
||||
|
201
docs/documentation/platform/ssh/usage.mdx
Normal file
201
docs/documentation/platform/ssh/usage.mdx
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: "Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Learn how to securely provision user SSH access to your infrastructure using SSH certificates."
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
Infisical SSH can be configured to provide users on your team short-lived, secure SSH access to infrastructure. Under the hood, it uses SSH certificates
|
||||
and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management,
|
||||
unauthorized access, and SSH key sprawl.
|
||||
|
||||
The following entities are important to understand when configuring and using Infisical SSH:
|
||||
|
||||
- Administrator: An individual on your team who is responsible for configuring Infisical SSH.
|
||||
- Users: Other individuals that gain access to remote hosts through Infisical SSH.
|
||||
- Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH.
|
||||
|
||||
## Workflow
|
||||
|
||||
The typical workflow for using Infisical SSH consists of the following steps:
|
||||
|
||||
1. The administrator registers a remote host with Infisical using the Infisical CLI via the `infisical ssh add-host` command.
|
||||
2. The administrator configures Infisical SSH to grant users access to the remote host.
|
||||
3. User(s) access the remote host using the Infisical CLI via the `infisical ssh connect` command.
|
||||
|
||||
## Admin Guide for Configuring Infisical SSH
|
||||
|
||||
In the following steps, we explore how to configure Infisical SSH to control and streamline your team's SSH access to infrastructure. As part of this guide,
|
||||
we will register a remote host with Infisical through a [machine identity](/documentation/platform/identities/machine-identities) and configure Infisical to grant user(s) access to the remote host.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an Infisical SSH project">
|
||||
Start by creating a new Infisical SSH project in Infisical.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Create a machine identity for bootstrapping Infisical SSH">
|
||||
2.1. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
|
||||
|
||||
By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the identity to authenticate with Infisical
|
||||
as part of registering a remote host in step 3.
|
||||
|
||||
<Note>
|
||||
You may use other authentication methods as suitable (e.g. [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), [GCP Auth](/documentation/platform/identities/gcp-auth), etc.) as part of the machine identity configuration but, to keep this example simple, we will be using Universal Auth.
|
||||
</Note>
|
||||
|
||||
2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the **SSH Host Bootstrapper** role.
|
||||
|
||||
This role grants the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical
|
||||
and establish the necessary configuration on it.
|
||||
|
||||
<Tip>
|
||||
If you plan to use a custom role to bootstrap SSH hosts, ensure the role has the **Create** and **Issue Host Certificates** on the **SSH Host** resource.
|
||||
</Tip>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Configure the remote host">
|
||||
3.1. Follow the instructions [here](/cli/overview) to install the Infisical CLI onto the remote host.
|
||||
|
||||
3.2. Run the commands below to register the remote host with Infisical.
|
||||
|
||||
Use the **Client ID** and **Client Secret** from the machine identity you created in step 2.1 as part of the `infisical login` command
|
||||
to obtain an access token and save it as an environment variable.
|
||||
|
||||
```bash
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain)
|
||||
```
|
||||
|
||||
Next, use the `infisical ssh add-host` command to register the remote host with Infisical. As part of this command, input the ID of the Infisical SSH project you created in step 1 for the `--projectId` flag and the hostname of the remote host for the `--hostname` flag.
|
||||
|
||||
```bash
|
||||
sudo infisical ssh add-host --projectId=<project-id> --hostname=<hostname> --token="$INFISICAL_TOKEN" --write-user-ca-to-file --write-host-cert-to-file --configure-sshd
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Note that if you're self-hosting Infisical, you can use the `--domain` flag on the `infisical login` command to specify the domain of your Infisical instance.
|
||||
|
||||
For more information on the `infisical ssh add-host` command, please refer to the Infisical CLI [documentation](/cli/overview).
|
||||
</Tip>
|
||||
|
||||
If successful, you should see output similar to the following:
|
||||
|
||||
```bash
|
||||
✅ Successfully registered host: <hostname>
|
||||
📁 Wrote User CA public key to: /etc/ssh/infisical_user_ca.pub
|
||||
📁 Wrote host certificate to: /etc/ssh/ssh_host_ed25519_key-cert.pub
|
||||
📄 Updated sshd_config entries
|
||||
```
|
||||
|
||||
Finally, use the following command to reload the SSH daemon on the remote host to apply the changes:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload sshd
|
||||
```
|
||||
|
||||
<Note>
|
||||
The command may differ depending on the host. For older versions of Ubuntu/Debian/CentOS, you may need to use `sudo service ssh reload` instead;
|
||||
for Alpine or minimal systems, `/etc/init.d/sshd reload`.
|
||||
</Note>
|
||||
|
||||
Back in Infisical, you should now see the remote host you just registered in the Infisical SSH project you created in step 1 under the **Hosts** tab.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Grant users access to the remote host">
|
||||
4.1. Add the user(s) you wish to grant access to the remote host to the Infisical SSH project under Access Control > Users.
|
||||
|
||||

|
||||
|
||||
4.2. On the registered host in the **Hosts** tab, click **Edit SSH Host** and add a login mapping for the user(s) you added in step 4.1.
|
||||
|
||||
The login mapping dictates what user(s) will be allowed access to the remote host and under a specific login user; in the allowed principals,
|
||||
you should select user(s) part of the Infisical SSH project that will be allowed to login to the remote host as the login user.
|
||||
|
||||
For instance, if you add a mapping with the login user `ec2-user` to some users John and Alice in Infisical, then they will be allowed to login to the remote host as `ec2-user` which is a system user that
|
||||
exists on the remote host.
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
Note that you should configure authorized principals files for each login user you add to the remote host.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## User Guide for SSHing to a Host
|
||||
|
||||
Once Infisical SSH is configured by an administrator, users can SSH to the remote host using the Infisical CLI.
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the Infisical CLI">
|
||||
Follow the instructions [here](/cli/overview) to install the Infisical CLI onto your local machine.
|
||||
</Step>
|
||||
<Step title="Connect to the remote host">
|
||||
The `infisical ssh connect` command can be used in either interactive or non-interactive mode to connect to a remote host.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Interactive Mode">
|
||||
In interactive mode, you'll first need to authenticate with Infisical by running:
|
||||
|
||||
```bash
|
||||
infisical login
|
||||
```
|
||||
|
||||
Then simply run:
|
||||
|
||||
```bash
|
||||
infisical ssh connect
|
||||
```
|
||||
|
||||
You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by
|
||||
the administrator.
|
||||
|
||||
```bash
|
||||
Use the arrow keys to navigate: ↓ ↑ → ←
|
||||
? Select an SSH Host:
|
||||
▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com
|
||||
```
|
||||
|
||||
After selecting a host, you'll be prompted to select a login user from a list of allowed login users:
|
||||
|
||||
```bash
|
||||
? Select Login User:
|
||||
▸ ec2-user
|
||||
```
|
||||
|
||||
If successful, you should be able to SSH to the remote host.
|
||||
|
||||
```bash
|
||||
✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com
|
||||
✔ ec2-user
|
||||
✔ SSH credentials successfully added to agent
|
||||
Connecting to ec2-user@ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com...
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Non-Interactive Mode">
|
||||
For CI/CD pipelines or automation scenarios, you can use the non-interactive mode with an Infisical token:
|
||||
|
||||
```bash
|
||||
infisical ssh connect \
|
||||
--hostname ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com \
|
||||
--login-user ec2-user \
|
||||
--out-file-path ~/.ssh/id_rsa-cert.pub \
|
||||
--token <your-infisical-token>
|
||||
```
|
||||
|
||||
This will:
|
||||
- Connect to the specified hostname
|
||||
- Use the specified login user
|
||||
- Write the SSH credentials to the specified path instead of adding them to the SSH agent
|
||||
- Authenticate using the provided Infisical token
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
</Steps>
|
@@ -196,6 +196,17 @@ The first deployment of Postgres based Infisical must be deployed with Docker im
|
||||
After deploying this version, you can proceed to update to any subsequent versions.
|
||||
</Warning>
|
||||
|
||||
## Additional discussion
|
||||
- When you visit Infisical's [docker hub](https://hub.docker.com/r/infisical/infisical) page, you will notice that image tags end with `-postgres`.
|
||||
This is to indicate that this version of Infisical runs on the new Postgres backend. Any image tag that does not end in `postgres` runs on MongoDB.
|
||||
## Important Notes
|
||||
|
||||
Infisical's [Docker Hub repository](https://hub.docker.com/r/infisical/infisical) uses different tagging conventions to indicate which database backend is used:
|
||||
|
||||
- **Before v0.46.11**
|
||||
- Infisical ran on MongoDB backend
|
||||
|
||||
- **After v0.46.11**
|
||||
- Version tags started to be suffixed with `-postgres`
|
||||
- Infisical transitioned to PostgreSQL backend
|
||||
|
||||
- **After v0.147.0**
|
||||
- Infisical remains on PostgreSQL backend
|
||||
- The `-postgres` suffix was removed from tags for brevity
|
66
docs/self-hosting/guides/releases.mdx
Normal file
66
docs/self-hosting/guides/releases.mdx
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "Release Channels"
|
||||
description: "Learn how to configure your deployment for different release schedules."
|
||||
---
|
||||
|
||||
Infisical uses rolling release channels to deliver new features, security fixes, and improvements with different update frequencies.
|
||||
This system allows you to balance getting the latest features with maintaining stability in your deployment environment.
|
||||
|
||||
## What are release channels?
|
||||
|
||||
Release channels define different schedules under which Infisical makes new releases available for you to deploy.
|
||||
Each channel operates on its own cadence, allowing you to choose how frequently you want to update your self-hosted deployment while balancing access to the latest features with your organization's stability requirements.
|
||||
|
||||
|
||||
## Available Channels
|
||||
|
||||
Infisical provides two distinct release channels with different update frequencies and stability profiles.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Nightly Channel">
|
||||
- **Update Frequency**: Daily builds during weekdays (Monday-Friday)
|
||||
- **Version Tags**: `vX.Y.Z-nightly-YYYYMMDD` (e.g., `v0.146.0-nightly-20250423`)
|
||||
- **Multiple Daily Builds**: If multiple nightly builds are created on the same day, they are numbered incrementally: `vX.Y.Z-nightly-YYYYMMDD.1`, `vX.Y.Z-nightly-YYYYMMDD.2`, etc.
|
||||
- **Stability**: Latest features with standard CI/CD testing
|
||||
- **Release Process**: Built from main branch after all automated tests pass
|
||||
- **Intended Audience**: Development environments & early adopters
|
||||
|
||||
**Best for:**
|
||||
- Organizations with flexible change management
|
||||
- Teams that want to test new features before they are made available via stable release channel
|
||||
|
||||
**Characteristics:**
|
||||
- Access to latest features immediately
|
||||
- Faster security patch delivery
|
||||
- Higher update frequency (daily)
|
||||
</Tab>
|
||||
<Tab title="Stable Channel">
|
||||
- **Update Frequency**: Monthly releases (typically 1st Tuesday of each month)
|
||||
- **Version Tags**: `vX.Y.Z` (e.g., `v0.145.0`, `v0.146.0`)
|
||||
- **Stability**: Both code tested and production-proven releases
|
||||
- **Release Process**: Features validated through nightly channel for 30+ days
|
||||
- **Intended Audience**: Production environments, enterprise teams prioritizing stability
|
||||
|
||||
**Best for:**
|
||||
- Enterprise environments with limited change windows
|
||||
- Organizations requiring predictable release cycles
|
||||
- Teams prioritizing stability and long-term support
|
||||
|
||||
**Characteristics:**
|
||||
- Predictable monthly schedule
|
||||
- Extensive testing and validation
|
||||
- Production-proven features
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
Schedule Note: Target dates are approximate and subject to change based on critical issues, security updates, or maintenance requirements.
|
||||
</Warning>
|
||||
|
||||
## Staying up to date
|
||||
|
||||
Track what features are available in each version across different sources:
|
||||
|
||||
- **Released versions**: View detailed changelogs, new features, and breaking changes in our [GitHub Releases](https://github.com/Infisical/infisical/releases)
|
||||
- **Docker Image Versions**: Browse available container images and tags on [Docker Hub](https://hub.docker.com/r/infisical/infisical)
|
||||
- **Linux Package Releases**: Access downloadable packages and version history on our [Linux releases page](https://cloudsmith.io/~infisical/repos/infisical-core/packages/)
|
@@ -125,7 +125,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Spinner size="xs" />
|
||||
<Spinner className="text-mineshaft-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ export type DrawerContentProps = DialogPrimitive.DialogContentProps & {
|
||||
subTitle?: ReactNode;
|
||||
footerContent?: ReactNode;
|
||||
onClose?: () => void;
|
||||
cardBodyClassName?: string;
|
||||
} & VariantProps<typeof drawerContentVariation>;
|
||||
|
||||
const drawerContentVariation = cva(
|
||||
@@ -32,7 +33,17 @@ const drawerContentVariation = cva(
|
||||
|
||||
export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
(
|
||||
{ children, title, subTitle, className, footerContent, direction = "right", onClose, ...props },
|
||||
{
|
||||
children,
|
||||
title,
|
||||
subTitle,
|
||||
className,
|
||||
footerContent,
|
||||
direction = "right",
|
||||
onClose,
|
||||
cardBodyClassName,
|
||||
...props
|
||||
},
|
||||
forwardedRef
|
||||
) => (
|
||||
<DialogPrimitive.Portal>
|
||||
@@ -47,11 +58,16 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
>
|
||||
<Card isRounded={false} className="dark h-full w-full">
|
||||
{title && (
|
||||
<CardTitle subTitle={subTitle} className="px-4">
|
||||
<CardTitle subTitle={subTitle} className="mb-0 px-4">
|
||||
{title}
|
||||
</CardTitle>
|
||||
)}
|
||||
<CardBody className="flex-grow overflow-y-auto overflow-x-hidden px-4 dark:[color-scheme:dark]">
|
||||
<CardBody
|
||||
className={twMerge(
|
||||
"flex-grow overflow-y-auto overflow-x-hidden px-4 pt-4 dark:[color-scheme:dark]",
|
||||
cardBodyClassName
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</CardBody>
|
||||
{footerContent && <CardFooter>{footerContent}</CardFooter>}{" "}
|
||||
|
@@ -1,2 +1 @@
|
||||
export { useOrgAdminAccessProject } from "./mutation";
|
||||
export { useOrgAdminGetProjects } from "./queries";
|
||||
|
@@ -1,30 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { Workspace } from "../types";
|
||||
import { TOrgAdminGetProjectsDTO } from "./types";
|
||||
|
||||
export const orgAdminQueryKeys = {
|
||||
getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const
|
||||
};
|
||||
|
||||
export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => {
|
||||
return useQuery({
|
||||
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
|
||||
"/api/v1/organization-admin/projects",
|
||||
{
|
||||
params: {
|
||||
limit,
|
||||
offset,
|
||||
search
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,9 +1,3 @@
|
||||
export type TOrgAdminGetProjectsDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export type TOrgAdminAccessProjectDTO = {
|
||||
projectId: string;
|
||||
};
|
||||
|
@@ -88,7 +88,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
|
||||
path: el.secretPath,
|
||||
secretMetadata: el.secretMetadata,
|
||||
isRotatedSecret: el.isRotatedSecret,
|
||||
rotationId: el.rotationId
|
||||
rotationId: el.rotationId,
|
||||
reminder: el.reminder
|
||||
};
|
||||
|
||||
if (el.type === SecretType.Personal) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { ProjectPermissionActions } from "@app/context";
|
||||
import { Reminder } from "@app/hooks/api/reminders/types";
|
||||
|
||||
import { PendingAction } from "../secretFolders/types";
|
||||
import type { WsTag } from "../tags/types";
|
||||
@@ -69,6 +70,7 @@ export type SecretV3RawSanitized = {
|
||||
rotationId?: string;
|
||||
isPending?: boolean;
|
||||
pendingAction?: PendingAction;
|
||||
reminder?: Reminder;
|
||||
};
|
||||
|
||||
export type SecretV3Raw = {
|
||||
@@ -94,6 +96,7 @@ export type SecretV3Raw = {
|
||||
isRotatedSecret?: boolean;
|
||||
rotationId?: string;
|
||||
secretReminderRecipients?: SecretReminderRecipient[];
|
||||
reminder?: Reminder;
|
||||
};
|
||||
|
||||
export type SecretV3RawResponse = {
|
||||
|
@@ -7,7 +7,6 @@ import {
|
||||
faPlug,
|
||||
faShare,
|
||||
faTable,
|
||||
faUserCog,
|
||||
faUsers,
|
||||
faUserTie
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -181,18 +180,6 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
Share Secret
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/admin">
|
||||
<MenuItem
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faUserCog} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Organization Admin
|
||||
</MenuItem>
|
||||
</Link>
|
||||
{user.superAdmin && (
|
||||
<Link to="/admin">
|
||||
<MenuItem
|
||||
|
@@ -1,39 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
|
||||
import { OrgAdminProjects } from "./components/OrgAdminProjects";
|
||||
|
||||
enum TabSections {
|
||||
Projects = "projects"
|
||||
}
|
||||
|
||||
export const AdminPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Projects);
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Organization Admin Console"
|
||||
description="View and manage resources across your organization."
|
||||
/>
|
||||
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Projects}>Projects</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Projects}>
|
||||
<OrgAdminProjects />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -1,169 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
Input,
|
||||
Pagination,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionAdminConsoleAction,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
export const OrgAdminProjects = withPermission(
|
||||
() => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch] = useDebounce(search);
|
||||
const [perPage, setPerPage] = useState(25);
|
||||
const navigate = useNavigate();
|
||||
const orgAdminAccessProject = useOrgAdminAccessProject();
|
||||
|
||||
const { data, isPending: isProjectsLoading } = useOrgAdminGetProjects({
|
||||
offset: (page - 1) * perPage,
|
||||
limit: perPage,
|
||||
search: debouncedSearch || undefined
|
||||
});
|
||||
|
||||
const projects = data?.projects || [];
|
||||
const projectCount = data?.count || 0;
|
||||
const isEmpty = !isProjectsLoading && projects.length === 0;
|
||||
|
||||
const handleAccessProject = async (type: ProjectType, projectId: string) => {
|
||||
try {
|
||||
await orgAdminAccessProject.mutateAsync({
|
||||
projectId
|
||||
});
|
||||
await navigate({
|
||||
to: getProjectHomePage(type),
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to access project",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-projects"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Projects</p>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search by project name"
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th>Created At</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
|
||||
{!isProjectsLoading &&
|
||||
projects?.map(({ name, slug, createdAt, id, type }) => (
|
||||
<Tr key={`project-${id}`} className="group w-full">
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")}</Td>
|
||||
<Td>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-bunker-300 hover:text-primary-400 data-[state=open]:text-primary-400"
|
||||
>
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleAccessProject(type, id);
|
||||
}}
|
||||
icon={<FontAwesomeIcon icon={faSignIn} />}
|
||||
disabled={
|
||||
orgAdminAccessProject.variables?.projectId === id &&
|
||||
orgAdminAccessProject.isPending
|
||||
}
|
||||
>
|
||||
Access{" "}
|
||||
{orgAdminAccessProject.variables?.projectId === id &&
|
||||
orgAdminAccessProject.isPending && <Spinner size="xs" />}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isProjectsLoading && (
|
||||
<Pagination
|
||||
count={projectCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{isEmpty && <EmptyState title="No projects found" />}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionAdminConsoleAction.AccessAllProjects,
|
||||
subject: OrgPermissionSubjects.AdminConsole
|
||||
}
|
||||
);
|
@@ -1 +0,0 @@
|
||||
export { OrgAdminProjects } from "./OrgAdminProjects";
|
@@ -1,16 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { AdminPage } from "./AdminPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/admin"
|
||||
)({
|
||||
component: AdminPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Admin Console"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
@@ -35,7 +35,8 @@ export const LogsSection = withPermission(
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId
|
||||
actor: presets?.actorId,
|
||||
eventMetadata: presets?.eventMetadata
|
||||
});
|
||||
const [timezone, setTimezone] = useState<Timezone>(Timezone.Local);
|
||||
|
||||
|
@@ -35,6 +35,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionAdminConsoleAction } from "@app/context/OrgPermissionContext/types";
|
||||
import { getProjectHomePage, getProjectLottieIcon, getProjectTitle } from "@app/helpers/project";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
@@ -42,7 +43,11 @@ import {
|
||||
setUserTablePreference
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useRequestProjectAccess, useSearchProjects } from "@app/hooks/api";
|
||||
import {
|
||||
useOrgAdminAccessProject,
|
||||
useRequestProjectAccess,
|
||||
useSearchProjects
|
||||
} from "@app/hooks/api";
|
||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type Props = {
|
||||
@@ -120,6 +125,8 @@ export const AllProjectView = ({
|
||||
initPerPage: getUserTablePreference("allProjectsTable", PreferenceKey.PerPage, 50)
|
||||
});
|
||||
|
||||
const orgAdminAccessProject = useOrgAdminAccessProject();
|
||||
|
||||
const handlePerPageChange = (newPerPage: number) => {
|
||||
setPerPage(newPerPage);
|
||||
setUserTablePreference("allProjectsTable", PreferenceKey.PerPage, newPerPage);
|
||||
@@ -137,6 +144,25 @@ export const AllProjectView = ({
|
||||
type: projectTypeFilter
|
||||
});
|
||||
|
||||
const handleAccessProject = async (type: ProjectType, projectId: string) => {
|
||||
try {
|
||||
await orgAdminAccessProject.mutateAsync({
|
||||
projectId
|
||||
});
|
||||
await navigate({
|
||||
to: getProjectHomePage(type),
|
||||
params: {
|
||||
projectId
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
text: "Failed to access project",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useResetPageHelper({
|
||||
setPage,
|
||||
offset,
|
||||
@@ -325,15 +351,38 @@ export const AllProjectView = ({
|
||||
<span>Joined</span>
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="opacity-0 transition-all group-hover:opacity-100">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
|
||||
>
|
||||
Request Access
|
||||
</Button>
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionAdminConsoleAction.AccessAllProjects}
|
||||
an={OrgPermissionSubjects.AdminConsole}
|
||||
>
|
||||
{(isAllowed) =>
|
||||
isAllowed ? (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleAccessProject(workspace.type, workspace.id);
|
||||
}}
|
||||
disabled={
|
||||
orgAdminAccessProject.variables?.projectId === workspace.id &&
|
||||
orgAdminAccessProject.isPending
|
||||
}
|
||||
>
|
||||
Join as Admin
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => handlePopUpOpen("requestAccessConfirmation", workspace)}
|
||||
>
|
||||
Request Access
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
@@ -97,7 +97,7 @@ import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||
import { FolderForm } from "./FolderForm";
|
||||
import { MoveSecretsModal } from "./MoveSecretsModal";
|
||||
|
||||
type TParsedEnv = Record<string, { value: string; comments: string[]; secretPath?: string }>;
|
||||
type TParsedEnv = { value: string; comments: string[]; secretPath?: string; secretKey: string }[];
|
||||
type TParsedFolderEnv = Record<
|
||||
string,
|
||||
Record<string, { value: string; comments: string[]; secretPath?: string }>
|
||||
@@ -405,8 +405,8 @@ export const ActionBar = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const allUpdateSecrets: TParsedEnv = {};
|
||||
const allCreateSecrets: TParsedEnv = {};
|
||||
const allUpdateSecrets: TParsedEnv = [];
|
||||
const allCreateSecrets: TParsedEnv = [];
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(envByPath).map(async ([folderPath, secrets]) => {
|
||||
@@ -437,7 +437,7 @@ export const ActionBar = ({
|
||||
(_, i) => secretFolderKeys.slice(i * batchSize, (i + 1) * batchSize)
|
||||
);
|
||||
|
||||
const existingSecretLookup: Record<string, boolean> = {};
|
||||
const existingSecretLookup = new Set<string>();
|
||||
|
||||
const processBatches = async () => {
|
||||
await secretBatches.reduce(async (previous, batch) => {
|
||||
@@ -451,7 +451,7 @@ export const ActionBar = ({
|
||||
});
|
||||
|
||||
batchSecrets.forEach((secret) => {
|
||||
existingSecretLookup[secret.secretKey] = true;
|
||||
existingSecretLookup.add(`${normalizedPath}-${secret.secretKey}`);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
};
|
||||
@@ -465,18 +465,18 @@ export const ActionBar = ({
|
||||
// Store the path with the secret for later batch processing
|
||||
const secretWithPath = {
|
||||
...secretData,
|
||||
secretPath: normalizedPath
|
||||
secretPath: normalizedPath,
|
||||
secretKey
|
||||
};
|
||||
|
||||
if (existingSecretLookup[secretKey]) {
|
||||
allUpdateSecrets[secretKey] = secretWithPath;
|
||||
if (existingSecretLookup.has(`${normalizedPath}-${secretKey}`)) {
|
||||
allUpdateSecrets.push(secretWithPath);
|
||||
} else {
|
||||
allCreateSecrets[secretKey] = secretWithPath;
|
||||
allCreateSecrets.push(secretWithPath);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
handlePopUpOpen("confirmUpload", {
|
||||
update: allUpdateSecrets,
|
||||
create: allCreateSecrets
|
||||
@@ -519,7 +519,7 @@ export const ActionBar = ({
|
||||
const allPaths = new Set<string>();
|
||||
|
||||
// Add paths from create secrets
|
||||
Object.values(create || {}).forEach((secData) => {
|
||||
create.forEach((secData) => {
|
||||
if (secData.secretPath && secData.secretPath !== secretPath) {
|
||||
allPaths.add(secData.secretPath);
|
||||
}
|
||||
@@ -575,8 +575,8 @@ export const ActionBar = ({
|
||||
return Promise.resolve();
|
||||
}, Promise.resolve());
|
||||
|
||||
if (Object.keys(create || {}).length > 0) {
|
||||
Object.entries(create).forEach(([secretKey, secData]) => {
|
||||
if (create.length > 0) {
|
||||
create.forEach((secData) => {
|
||||
// Use the stored secretPath or fall back to the current secretPath
|
||||
const path = secData.secretPath || secretPath;
|
||||
|
||||
@@ -588,7 +588,7 @@ export const ActionBar = ({
|
||||
type: SecretType.Shared,
|
||||
secretComment: secData.comments.join("\n"),
|
||||
secretValue: secData.value,
|
||||
secretKey
|
||||
secretKey: secData.secretKey
|
||||
});
|
||||
});
|
||||
|
||||
@@ -604,8 +604,8 @@ export const ActionBar = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(update || {}).length > 0) {
|
||||
Object.entries(update).forEach(([secretKey, secData]) => {
|
||||
if (update.length > 0) {
|
||||
update.forEach((secData) => {
|
||||
// Use the stored secretPath or fall back to the current secretPath
|
||||
const path = secData.secretPath || secretPath;
|
||||
|
||||
@@ -617,7 +617,7 @@ export const ActionBar = ({
|
||||
type: SecretType.Shared,
|
||||
secretComment: secData.comments.join("\n"),
|
||||
secretValue: secData.value,
|
||||
secretKey
|
||||
secretKey: secData.secretKey
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1229,8 +1229,8 @@ export const ActionBar = ({
|
||||
<div className="flex flex-col text-gray-300">
|
||||
<div>Your project already contains the following {updateSecretCount} secrets:</div>
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
|
||||
?.map((key) => key)
|
||||
{(popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update
|
||||
?.map((sec) => sec.secretKey)
|
||||
.join(", ")}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,9 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
FontAwesomeSymbol,
|
||||
FormControl,
|
||||
GenericFieldLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalTrigger,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
@@ -41,16 +39,14 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { memo, useCallback, useEffect, useRef } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import {
|
||||
hasSecretReference,
|
||||
SecretReferenceTree
|
||||
} from "@app/components/secrets/SecretReferenceDetails";
|
||||
|
||||
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||
import { format } from "date-fns";
|
||||
import { CreateReminderForm } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm";
|
||||
import {
|
||||
FontAwesomeSpriteName,
|
||||
formSchema,
|
||||
@@ -113,7 +109,8 @@ export const SecretItem = memo(
|
||||
colWidth
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"editSecret"
|
||||
"editSecret",
|
||||
"reminder"
|
||||
] as const);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { permission } = useProjectPermission();
|
||||
@@ -186,7 +183,7 @@ export const SecretItem = memo(
|
||||
name: "tags"
|
||||
});
|
||||
|
||||
const isOverriden =
|
||||
const isOverridden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
const hasTagsApplied = Boolean(fields.length);
|
||||
|
||||
@@ -272,7 +269,7 @@ export const SecretItem = memo(
|
||||
}, [isSecValueCopied]);
|
||||
|
||||
const handleOverrideClick = () => {
|
||||
if (isOverriden) {
|
||||
if (isOverridden) {
|
||||
// override need not be flagged delete if it was never saved in server
|
||||
// meaning a new unsaved personal secret but user toggled back later
|
||||
const isUnsavedOverride = !secret.idOverride;
|
||||
@@ -327,7 +324,7 @@ export const SecretItem = memo(
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
const [overrideValue, value] = getValues(["value", "valueOverride"]);
|
||||
if (isOverriden) {
|
||||
if (isOverridden) {
|
||||
navigator.clipboard.writeText(value as string);
|
||||
} else {
|
||||
navigator.clipboard.writeText(overrideValue as string);
|
||||
@@ -335,7 +332,7 @@ export const SecretItem = memo(
|
||||
setIsSecValueCopied.on();
|
||||
};
|
||||
|
||||
const isInAutoSaveMode = isDirty && !isSubmitting && !isOverriden;
|
||||
const isInAutoSaveMode = isDirty && !isSubmitting && !isOverridden;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
@@ -398,7 +395,7 @@ export const SecretItem = memo(
|
||||
isReadOnly={isReadOnly || isRotatedSecret}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
variant="plain"
|
||||
isDisabled={isOverriden}
|
||||
isDisabled={isOverridden}
|
||||
placeholder={error?.message}
|
||||
isError={Boolean(error)}
|
||||
onKeyUp={() => trigger("key")}
|
||||
@@ -413,14 +410,14 @@ export const SecretItem = memo(
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{secretValueHidden && !isOverriden && !isPending && (
|
||||
{secretValueHidden && !isOverridden && !isPending && (
|
||||
<Tooltip
|
||||
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
|
||||
>
|
||||
<FontAwesomeIcon className="pr-2" size="sm" icon={faEyeSlash} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{isOverriden ? (
|
||||
{isOverridden ? (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
key="value-overriden"
|
||||
@@ -445,7 +442,7 @@ export const SecretItem = memo(
|
||||
isReadOnly={isReadOnlySecret}
|
||||
key="secret-value"
|
||||
isVisible={isVisible && (!secretValueHidden || isPending)}
|
||||
canEditButNotView={secretValueHidden && !isOverriden && !isPending}
|
||||
canEditButNotView={secretValueHidden && !isOverridden && !isPending}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
{...field}
|
||||
@@ -462,15 +459,15 @@ export const SecretItem = memo(
|
||||
key="actions"
|
||||
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
|
||||
>
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden}
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden}
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<Tooltip content="Copy secret">
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3"
|
||||
symbolName={
|
||||
@@ -479,8 +476,8 @@ export const SecretItem = memo(
|
||||
: FontAwesomeSpriteName.ClipboardCopy
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
@@ -491,41 +488,47 @@ export const SecretItem = memo(
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<IconButton
|
||||
className="w-0 overflow-hidden p-0 group-hover:w-5"
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="reference-tree"
|
||||
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
|
||||
>
|
||||
<Tooltip
|
||||
content={
|
||||
hasSecretReference(secret?.value)
|
||||
? "Secret Reference Tree"
|
||||
: "Secret does not contain references"
|
||||
}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.SecretReferenceTree}
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
title="Secret Reference Details"
|
||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
secret.reminder && "w-5 text-primary"
|
||||
)}
|
||||
onClick={() => handlePopUpOpen("reminder")}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="Secret reminder"
|
||||
isDisabled={!isAllowed || isOverridden}
|
||||
>
|
||||
<Tooltip
|
||||
className="max-w-2xl"
|
||||
content={
|
||||
isOverridden ? (
|
||||
"Unavailable with override"
|
||||
) : secret.reminder ? (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<GenericFieldLabel label="Reminder Date">
|
||||
{secret.reminder.nextReminderDate
|
||||
? format(
|
||||
new Date(secret.reminder.nextReminderDate),
|
||||
"h:mm aa - MMM d yyyy"
|
||||
)
|
||||
: undefined}
|
||||
</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Message">
|
||||
{secret.reminder.message}
|
||||
</GenericFieldLabel>
|
||||
</div>
|
||||
) : (
|
||||
"Set Secret Reminder"
|
||||
)
|
||||
}
|
||||
>
|
||||
<SecretReferenceTree
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
secretKey={secret?.key}
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Reminder}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenu>
|
||||
@@ -539,7 +542,7 @@ export const SecretItem = memo(
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed || isOverridden}>
|
||||
<IconButton
|
||||
ariaLabel="tags"
|
||||
variant="plain"
|
||||
@@ -548,9 +551,9 @@ export const SecretItem = memo(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || isOverridden}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<Tooltip content={isOverridden ? "Unavailable with override" : "Tags"}>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Tags}
|
||||
@@ -617,8 +620,6 @@ export const SecretItem = memo(
|
||||
secretName,
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Override"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -629,13 +630,15 @@ export const SecretItem = memo(
|
||||
onClick={handleOverrideClick}
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
isOverriden && "w-5 text-primary"
|
||||
isOverridden && "w-5 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Override}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<Tooltip content={`${isOverridden ? "Remove" : "Add"} Override`}>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Override}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -650,7 +653,7 @@ export const SecretItem = memo(
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<PopoverTrigger asChild disabled={!isAllowed || isOverridden}>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"w-0 overflow-hidden p-0 group-hover:w-5",
|
||||
@@ -659,9 +662,11 @@ export const SecretItem = memo(
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-comment"
|
||||
isDisabled={!isAllowed}
|
||||
isDisabled={!isAllowed || isOverridden}
|
||||
>
|
||||
<Tooltip content="Comment">
|
||||
<Tooltip
|
||||
content={isOverridden ? "Unavailable with override" : "Comment"}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
className="h-3.5 w-3.5"
|
||||
symbolName={FontAwesomeSpriteName.Comment}
|
||||
@@ -851,12 +856,13 @@ export const SecretItem = memo(
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="More">
|
||||
<Tooltip content={isOverridden ? "Unavailable with override" : "More"}>
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
||||
isDisabled={isOverridden}
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
@@ -874,7 +880,13 @@ export const SecretItem = memo(
|
||||
secretTags: selectedTagSlugs
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"}
|
||||
allowedLabel={
|
||||
isOverridden
|
||||
? "Unavailable with override"
|
||||
: isRotatedSecret
|
||||
? "Cannot Delete Rotated Secret"
|
||||
: "Delete"
|
||||
}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -884,7 +896,7 @@ export const SecretItem = memo(
|
||||
size="md"
|
||||
className="p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed || isRotatedSecret}
|
||||
isDisabled={!isAllowed || isRotatedSecret || isOverridden}
|
||||
>
|
||||
<FontAwesomeSymbol
|
||||
symbolName={FontAwesomeSpriteName.Trash}
|
||||
@@ -959,6 +971,15 @@ export const SecretItem = memo(
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
<CreateReminderForm
|
||||
isOpen={popUp.reminder.isOpen}
|
||||
onOpenChange={() => handlePopUpToggle("reminder")}
|
||||
workspaceId={currentWorkspace.id}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
secretId={secret?.id}
|
||||
reminder={secret.reminder}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.editSecret.isOpen}
|
||||
deleteKey="confirm"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faBell,
|
||||
faCheck,
|
||||
faClock,
|
||||
faClone,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
faEllipsis,
|
||||
faKey,
|
||||
faLock,
|
||||
faProjectDiagram,
|
||||
faShare,
|
||||
faTags,
|
||||
faTrash
|
||||
@@ -83,7 +83,7 @@ export enum FontAwesomeSpriteName {
|
||||
ReplicatedSecretKey = "secret-replicated",
|
||||
ShareSecret = "share-secret",
|
||||
KeyLock = "key-lock",
|
||||
SecretReferenceTree = "secret-reference-tree",
|
||||
Reminder = "secret-reminder",
|
||||
Trash = "trash"
|
||||
}
|
||||
|
||||
@@ -103,6 +103,6 @@ export const FontAwesomeSpriteSymbols = [
|
||||
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
|
||||
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
|
||||
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
|
||||
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree },
|
||||
{ icon: faBell, symbol: FontAwesomeSpriteName.Reminder },
|
||||
{ icon: faTrash, symbol: FontAwesomeSpriteName.Trash }
|
||||
];
|
||||
|
@@ -50,7 +50,6 @@ import { Route as adminAuthenticationPageRouteImport } from './pages/admin/Authe
|
||||
import { Route as organizationProjectsPageRouteImport } from './pages/organization/ProjectsPage/route'
|
||||
import { Route as organizationBillingPageRouteImport } from './pages/organization/BillingPage/route'
|
||||
import { Route as organizationAuditLogsPageRouteImport } from './pages/organization/AuditLogsPage/route'
|
||||
import { Route as organizationAdminPageRouteImport } from './pages/organization/AdminPage/route'
|
||||
import { Route as organizationAccessManagementPageRouteImport } from './pages/organization/AccessManagementPage/route'
|
||||
import { Route as adminGeneralPageRouteImport } from './pages/admin/GeneralPage/route'
|
||||
import { Route as secretManagerRedirectsRedirectApprovalPageImport } from './pages/secret-manager/redirects/redirect-approval-page'
|
||||
@@ -631,15 +630,6 @@ const organizationAuditLogsPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const organizationAdminPageRouteRoute = organizationAdminPageRouteImport.update(
|
||||
{
|
||||
id: '/admin',
|
||||
path: '/admin',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any,
|
||||
)
|
||||
|
||||
const organizationAccessManagementPageRouteRoute =
|
||||
organizationAccessManagementPageRouteImport.update({
|
||||
id: '/access-management',
|
||||
@@ -2293,13 +2283,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof organizationAccessManagementPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/admin': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/admin'
|
||||
path: '/admin'
|
||||
fullPath: '/organization/admin'
|
||||
preLoaderRoute: typeof organizationAdminPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
|
||||
path: '/audit-logs'
|
||||
@@ -3758,7 +3741,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
|
||||
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
|
||||
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
|
||||
organizationAuditLogsPageRouteRoute: typeof organizationAuditLogsPageRouteRoute
|
||||
organizationBillingPageRouteRoute: typeof organizationBillingPageRouteRoute
|
||||
organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute
|
||||
@@ -3776,7 +3758,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
|
||||
{
|
||||
organizationAccessManagementPageRouteRoute:
|
||||
organizationAccessManagementPageRouteRoute,
|
||||
organizationAdminPageRouteRoute: organizationAdminPageRouteRoute,
|
||||
organizationAuditLogsPageRouteRoute: organizationAuditLogsPageRouteRoute,
|
||||
organizationBillingPageRouteRoute: organizationBillingPageRouteRoute,
|
||||
organizationProjectsPageRouteRoute: organizationProjectsPageRouteRoute,
|
||||
@@ -4668,7 +4649,6 @@ export interface FileRoutesByFullPath {
|
||||
'/organization': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren
|
||||
'/admin/': typeof adminGeneralPageRouteRoute
|
||||
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
|
||||
'/organization/admin': typeof organizationAdminPageRouteRoute
|
||||
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
|
||||
'/organization/billing': typeof organizationBillingPageRouteRoute
|
||||
'/organization/projects': typeof organizationProjectsPageRouteRoute
|
||||
@@ -4887,7 +4867,6 @@ export interface FileRoutesByTo {
|
||||
'/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutIntegrationsRouteWithChildren
|
||||
'/organization': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteWithChildren
|
||||
'/organization/access-management': typeof organizationAccessManagementPageRouteRoute
|
||||
'/organization/admin': typeof organizationAdminPageRouteRoute
|
||||
'/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
|
||||
'/organization/billing': typeof organizationBillingPageRouteRoute
|
||||
'/organization/projects': typeof organizationProjectsPageRouteRoute
|
||||
@@ -5106,7 +5085,6 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout': typeof adminLayoutRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/admin/_admin-layout/': typeof adminGeneralPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/access-management': typeof organizationAccessManagementPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/admin': typeof organizationAdminPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/audit-logs': typeof organizationAuditLogsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/billing': typeof organizationBillingPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/projects': typeof organizationProjectsPageRouteRoute
|
||||
@@ -5336,7 +5314,6 @@ export interface FileRouteTypes {
|
||||
| '/organization'
|
||||
| '/admin/'
|
||||
| '/organization/access-management'
|
||||
| '/organization/admin'
|
||||
| '/organization/audit-logs'
|
||||
| '/organization/billing'
|
||||
| '/organization/projects'
|
||||
@@ -5554,7 +5531,6 @@ export interface FileRouteTypes {
|
||||
| '/integrations'
|
||||
| '/organization'
|
||||
| '/organization/access-management'
|
||||
| '/organization/admin'
|
||||
| '/organization/audit-logs'
|
||||
| '/organization/billing'
|
||||
| '/organization/projects'
|
||||
@@ -5771,7 +5747,6 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout'
|
||||
| '/_authenticate/_inject-org-details/admin/_admin-layout/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/access-management'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/admin'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/audit-logs'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/billing'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/projects'
|
||||
@@ -6202,7 +6177,6 @@ export const routeTree = rootRoute
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/access-management",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/admin",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/billing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/projects",
|
||||
@@ -6239,10 +6213,6 @@ export const routeTree = rootRoute
|
||||
"filePath": "organization/AccessManagementPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/admin": {
|
||||
"filePath": "organization/AdminPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/audit-logs": {
|
||||
"filePath": "organization/AuditLogsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
|
@@ -20,7 +20,6 @@ const adminRoute = route("/admin", [
|
||||
const organizationRoutes = route("/organization", [
|
||||
route("/projects", "organization/ProjectsPage/route.tsx"),
|
||||
route("/access-management", "organization/AccessManagementPage/route.tsx"),
|
||||
route("/admin", "organization/AdminPage/route.tsx"),
|
||||
route("/audit-logs", "organization/AuditLogsPage/route.tsx"),
|
||||
route("/billing", "organization/BillingPage/route.tsx"),
|
||||
route("/secret-sharing", [
|
||||
|
@@ -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.9.5
|
||||
version: v0.10.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.9.5"
|
||||
appVersion: "v0.10.0"
|
||||
|
@@ -4,7 +4,7 @@ kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: clustergenerators.secrets.infisical.com
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
@@ -22,14 +22,19 @@ spec:
|
||||
description: ClusterGenerator represents a cluster-wide generator
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -47,27 +52,29 @@ spec:
|
||||
description: set allowRepeat to true to allow repeating characters.
|
||||
type: boolean
|
||||
digits:
|
||||
description: digits specifies the number of digits in the generated
|
||||
password. If omitted it defaults to 25% of the length of the
|
||||
password
|
||||
description: |-
|
||||
digits specifies the number of digits in the generated
|
||||
password. If omitted it defaults to 25% of the length of the password
|
||||
type: integer
|
||||
length:
|
||||
default: 24
|
||||
description: Length of the password to be generated. Defaults
|
||||
to 24
|
||||
description: |-
|
||||
Length of the password to be generated.
|
||||
Defaults to 24
|
||||
type: integer
|
||||
noUpper:
|
||||
default: false
|
||||
description: Set noUpper to disable uppercase characters
|
||||
type: boolean
|
||||
symbolCharacters:
|
||||
description: symbolCharacters specifies the special characters
|
||||
that should be used in the generated password.
|
||||
description: |-
|
||||
symbolCharacters specifies the special characters that should be used
|
||||
in the generated password.
|
||||
type: string
|
||||
symbols:
|
||||
description: symbols specifies the number of symbol characters
|
||||
in the generated password. If omitted it defaults to 25% of
|
||||
the length of the password
|
||||
description: |-
|
||||
symbols specifies the number of symbol characters in the generated
|
||||
password. If omitted it defaults to 25% of the length of the password
|
||||
type: integer
|
||||
type: object
|
||||
uuidSpec:
|
||||
|
@@ -3,57 +3,25 @@ kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-controller-manager
|
||||
labels:
|
||||
app.kubernetes.io/component: manager
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
control-plane: controller-manager
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.controllerManager.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: k8-operator
|
||||
control-plane: controller-manager
|
||||
{{- include "secrets-operator.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: k8-operator
|
||||
control-plane: controller-manager
|
||||
{{- include "secrets-operator.selectorLabels" . | nindent 8 }}
|
||||
annotations:
|
||||
kubectl.kubernetes.io/default-container: manager
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/arch
|
||||
operator: In
|
||||
values:
|
||||
- amd64
|
||||
- arm64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- key: kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
containers:
|
||||
- args: {{- toYaml .Values.controllerManager.kubeRbacProxy.args | nindent 8 }}
|
||||
env:
|
||||
- name: KUBERNETES_CLUSTER_DOMAIN
|
||||
value: {{ quote .Values.kubernetesClusterDomain }}
|
||||
image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag
|
||||
| default .Chart.AppVersion }}
|
||||
name: kube-rbac-proxy
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
name: https
|
||||
protocol: TCP
|
||||
resources: {{- toYaml .Values.controllerManager.kubeRbacProxy.resources | nindent
|
||||
10 }}
|
||||
securityContext: {{- toYaml .Values.controllerManager.kubeRbacProxy.containerSecurityContext
|
||||
| nindent 10 }}
|
||||
- args:
|
||||
{{- toYaml .Values.controllerManager.manager.args | nindent 8 }}
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
|
@@ -0,0 +1,49 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-admin-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets
|
||||
verbs:
|
||||
- '*'
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-admin-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-admin-role'
|
@@ -4,7 +4,7 @@ kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: infisicaldynamicsecrets.secrets.infisical.com
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
@@ -23,14 +23,19 @@ spec:
|
||||
API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -75,11 +80,9 @@ spec:
|
||||
kubernetesAuth:
|
||||
properties:
|
||||
autoCreateServiceAccountToken:
|
||||
description: Optionally automatically create a service account
|
||||
token for the configured service account. If this is set to
|
||||
`true`, the operator will automatically create a service account
|
||||
token for the configured service account. This field is recommended
|
||||
in most cases.
|
||||
description: |-
|
||||
Optionally automatically create a service account token for the configured service account.
|
||||
If this is set to `true`, the operator will automatically create a service account token for the configured service account. This field is recommended in most cases.
|
||||
type: boolean
|
||||
identityId:
|
||||
type: string
|
||||
@@ -170,11 +173,11 @@ spec:
|
||||
properties:
|
||||
creationPolicy:
|
||||
default: Orphan
|
||||
description: 'The Kubernetes Secret creation policy. Enum with values:
|
||||
''Owner'', ''Orphan''. Owner creates the secret and sets .metadata.ownerReferences
|
||||
of the InfisicalSecret CRD that created it. Orphan will not set
|
||||
the secret owner. This will result in the secret being orphaned
|
||||
and not deleted when the resource is deleted.'
|
||||
description: |-
|
||||
The Kubernetes Secret creation policy.
|
||||
Enum with values: 'Owner', 'Orphan'.
|
||||
Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
|
||||
Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.
|
||||
type: string
|
||||
secretName:
|
||||
description: The name of the Kubernetes Secret
|
||||
@@ -196,9 +199,9 @@ spec:
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the top
|
||||
level of your template. Secrets defined in the template will
|
||||
take precedence over the injected ones.
|
||||
description: |-
|
||||
This injects all retrieved secrets into the top level of your template.
|
||||
Secrets defined in the template will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
@@ -239,44 +242,36 @@ spec:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource. --- This struct is intended for direct
|
||||
use as an array at the field path .status.conditions. For example,
|
||||
\n type FooStatus struct{ // Represents the observations of a foo's
|
||||
current state. // Known .status.conditions.type are: \"Available\",
|
||||
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating details
|
||||
about the transition. This may be an empty string.
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers of
|
||||
specific condition types may define expected values and meanings
|
||||
for this field, and whether the values are considered a guaranteed
|
||||
API. The value should be a CamelCase string. This field may
|
||||
not be empty.
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
@@ -290,10 +285,6 @@ spec:
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
|
@@ -0,0 +1,55 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-editor-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-editor-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-editor-role'
|
@@ -0,0 +1,51 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-viewer-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-viewer-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicaldynamicsecret-viewer-role'
|
@@ -4,7 +4,7 @@ kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: infisicalpushsecrets.secrets.infisical.com
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
@@ -23,14 +23,19 @@ spec:
|
||||
API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -75,11 +80,9 @@ spec:
|
||||
kubernetesAuth:
|
||||
properties:
|
||||
autoCreateServiceAccountToken:
|
||||
description: Optionally automatically create a service account
|
||||
token for the configured service account. If this is set to
|
||||
`true`, the operator will automatically create a service account
|
||||
token for the configured service account. This field is recommended
|
||||
in most cases.
|
||||
description: |-
|
||||
Optionally automatically create a service account token for the configured service account.
|
||||
If this is set to `true`, the operator will automatically create a service account token for the configured service account. This field is recommended in most cases.
|
||||
type: boolean
|
||||
identityId:
|
||||
type: string
|
||||
@@ -208,9 +211,9 @@ spec:
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the
|
||||
top level of your template. Secrets defined in the template
|
||||
will take precedence over the injected ones.
|
||||
description: |-
|
||||
This injects all retrieved secrets into the top level of your template.
|
||||
Secrets defined in the template will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
@@ -252,44 +255,36 @@ spec:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource. --- This struct is intended for direct
|
||||
use as an array at the field path .status.conditions. For example,
|
||||
\n type FooStatus struct{ // Represents the observations of a foo's
|
||||
current state. // Known .status.conditions.type are: \"Available\",
|
||||
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating details
|
||||
about the transition. This may be an empty string.
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers of
|
||||
specific condition types may define expected values and meanings
|
||||
for this field, and whether the values are considered a guaranteed
|
||||
API. The value should be a CamelCase string. This field may
|
||||
not be empty.
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
@@ -303,10 +298,6 @@ spec:
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
|
@@ -0,0 +1,49 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-admin-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecretsecrets
|
||||
verbs:
|
||||
- '*'
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecretsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-admin-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-admin-role'
|
@@ -0,0 +1,55 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-editor-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecretsecrets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecretsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-editor-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-editor-role'
|
@@ -0,0 +1,51 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-viewer-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecretsecrets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecretsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-viewer-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicalpushsecretsecret-viewer-role'
|
@@ -0,0 +1,49 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalsecret-admin-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalsecrets
|
||||
verbs:
|
||||
- '*'
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalsecret-admin-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicalsecret-admin-role'
|
@@ -4,7 +4,7 @@ kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: infisicalsecrets.secrets.infisical.com
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
@@ -22,14 +22,19 @@ spec:
|
||||
description: InfisicalSecret is the Schema for the infisicalsecrets API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -138,10 +143,9 @@ spec:
|
||||
kubernetesAuth:
|
||||
properties:
|
||||
autoCreateServiceAccountToken:
|
||||
description: Optionally automatically create a service account
|
||||
token for the configured service account. If this is set to
|
||||
`true`, the operator will automatically create a service account
|
||||
token for the configured service account.
|
||||
description: |-
|
||||
Optionally automatically create a service account token for the configured service account.
|
||||
If this is set to `true`, the operator will automatically create a service account token for the configured service account.
|
||||
type: boolean
|
||||
identityId:
|
||||
type: string
|
||||
@@ -323,12 +327,11 @@ spec:
|
||||
type: string
|
||||
creationPolicy:
|
||||
default: Orphan
|
||||
description: 'The Kubernetes ConfigMap creation policy. Enum with
|
||||
values: ''Owner'', ''Orphan''. Owner creates the config map
|
||||
and sets .metadata.ownerReferences of the InfisicalSecret CRD
|
||||
that created it. Orphan will not set the config map owner. This
|
||||
will result in the config map being orphaned and not deleted
|
||||
when the resource is deleted.'
|
||||
description: |-
|
||||
The Kubernetes ConfigMap creation policy.
|
||||
Enum with values: 'Owner', 'Orphan'.
|
||||
Owner creates the config map and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
|
||||
Orphan will not set the config map owner. This will result in the config map being orphaned and not deleted when the resource is deleted.
|
||||
type: string
|
||||
template:
|
||||
description: The template to transform the secret data
|
||||
@@ -339,9 +342,9 @@ spec:
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the top
|
||||
level of your template. Secrets defined in the template
|
||||
will take precedence over the injected ones.
|
||||
description: |-
|
||||
This injects all retrieved secrets into the top level of your template.
|
||||
Secrets defined in the template will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
@@ -354,12 +357,11 @@ spec:
|
||||
properties:
|
||||
creationPolicy:
|
||||
default: Orphan
|
||||
description: 'The Kubernetes Secret creation policy. Enum with
|
||||
values: ''Owner'', ''Orphan''. Owner creates the secret and
|
||||
sets .metadata.ownerReferences of the InfisicalSecret CRD that
|
||||
created it. Orphan will not set the secret owner. This will
|
||||
result in the secret being orphaned and not deleted when the
|
||||
resource is deleted.'
|
||||
description: |-
|
||||
The Kubernetes Secret creation policy.
|
||||
Enum with values: 'Owner', 'Orphan'.
|
||||
Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
|
||||
Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.
|
||||
type: string
|
||||
secretName:
|
||||
description: The name of the Kubernetes Secret
|
||||
@@ -381,9 +383,9 @@ spec:
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the top
|
||||
level of your template. Secrets defined in the template
|
||||
will take precedence over the injected ones.
|
||||
description: |-
|
||||
This injects all retrieved secrets into the top level of your template.
|
||||
Secrets defined in the template will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
@@ -395,11 +397,11 @@ spec:
|
||||
properties:
|
||||
creationPolicy:
|
||||
default: Orphan
|
||||
description: 'The Kubernetes Secret creation policy. Enum with values:
|
||||
''Owner'', ''Orphan''. Owner creates the secret and sets .metadata.ownerReferences
|
||||
of the InfisicalSecret CRD that created it. Orphan will not set
|
||||
the secret owner. This will result in the secret being orphaned
|
||||
and not deleted when the resource is deleted.'
|
||||
description: |-
|
||||
The Kubernetes Secret creation policy.
|
||||
Enum with values: 'Owner', 'Orphan'.
|
||||
Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
|
||||
Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.
|
||||
type: string
|
||||
secretName:
|
||||
description: The name of the Kubernetes Secret
|
||||
@@ -421,9 +423,9 @@ spec:
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the top
|
||||
level of your template. Secrets defined in the template will
|
||||
take precedence over the injected ones.
|
||||
description: |-
|
||||
This injects all retrieved secrets into the top level of your template.
|
||||
Secrets defined in the template will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
@@ -474,44 +476,36 @@ spec:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource. --- This struct is intended for direct
|
||||
use as an array at the field path .status.conditions. For example,
|
||||
\n type FooStatus struct{ // Represents the observations of a foo's
|
||||
current state. // Known .status.conditions.type are: \"Available\",
|
||||
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating details
|
||||
about the transition. This may be an empty string.
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers of
|
||||
specific condition types may define expected values and meanings
|
||||
for this field, and whether the values are considered a guaranteed
|
||||
API. The value should be a CamelCase string. This field may
|
||||
not be empty.
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
@@ -525,10 +519,6 @@ spec:
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
|
@@ -0,0 +1,55 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalsecret-editor-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalsecrets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalsecret-editor-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicalsecret-editor-role'
|
@@ -0,0 +1,51 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalsecret-viewer-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalsecrets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-infisicalsecret-viewer-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-infisicalsecret-viewer-role'
|
@@ -3,9 +3,6 @@ kind: Role
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-leader-election-role
|
||||
labels:
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
@@ -45,9 +42,6 @@ kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-leader-election-rolebinding
|
||||
labels:
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
@@ -16,6 +16,7 @@ rules:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
- secrets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
@@ -30,17 +31,6 @@ rules:
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
@@ -66,15 +56,6 @@ rules:
|
||||
- list
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
@@ -85,69 +66,8 @@ rules:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- clustergenerators
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecrets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecrets/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalpushsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicalsecrets
|
||||
verbs:
|
||||
- create
|
||||
@@ -160,12 +80,16 @@ rules:
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets/finalizers
|
||||
- infisicalpushsecrets/finalizers
|
||||
- infisicalsecrets/finalizers
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- secrets.infisical.com
|
||||
resources:
|
||||
- infisicaldynamicsecrets/status
|
||||
- infisicalpushsecrets/status
|
||||
- infisicalsecrets/status
|
||||
verbs:
|
||||
- get
|
||||
@@ -184,9 +108,7 @@ metadata:
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
|
@@ -0,0 +1,53 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-metrics-auth-role
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: RoleBinding
|
||||
{{- else }}
|
||||
kind: ClusterRoleBinding
|
||||
{{- end }}
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-metrics-auth-rolebinding
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
namespace: {{ .Values.scopedNamespace | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- if and .Values.scopedNamespace .Values.scopedRBAC }}
|
||||
kind: Role
|
||||
{{- else }}
|
||||
kind: ClusterRole
|
||||
{{- end }}
|
||||
name: '{{ include "secrets-operator.fullname" . }}-metrics-auth-role'
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: '{{ include "secrets-operator.fullname" . }}-controller-manager'
|
||||
namespace: '{{ .Release.Namespace }}'
|
@@ -4,9 +4,6 @@ kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-metrics-reader
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- nonResourceURLs:
|
||||
|
@@ -3,14 +3,12 @@ kind: Service
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-controller-manager-metrics-service
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
control-plane: controller-manager
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.metricsService.type }}
|
||||
selector:
|
||||
app.kubernetes.io/name: k8-operator
|
||||
control-plane: controller-manager
|
||||
{{- include "secrets-operator.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
|
@@ -1,43 +0,0 @@
|
||||
{{- if not .Values.scopedNamespace }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-proxy-role
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-proxy-rolebinding
|
||||
labels:
|
||||
app.kubernetes.io/component: kube-rbac-proxy
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: '{{ include "secrets-operator.fullname" . }}-proxy-role'
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: '{{ include "secrets-operator.fullname" . }}-controller-manager'
|
||||
namespace: '{{ .Release.Namespace }}'
|
||||
|
||||
{{- end }}
|
@@ -3,9 +3,6 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "secrets-operator.fullname" . }}-controller-manager
|
||||
labels:
|
||||
app.kubernetes.io/component: rbac
|
||||
app.kubernetes.io/created-by: k8-operator
|
||||
app.kubernetes.io/part-of: k8-operator
|
||||
{{- include "secrets-operator.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- toYaml .Values.controllerManager.serviceAccount.annotations | nindent 4 }}
|
||||
|
@@ -1,38 +1,18 @@
|
||||
controllerManager:
|
||||
kubeRbacProxy:
|
||||
args:
|
||||
- --secure-listen-address=0.0.0.0:8443
|
||||
- --upstream=http://127.0.0.1:8080/
|
||||
- --logtostderr=true
|
||||
- --v=0
|
||||
containerSecurityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
image:
|
||||
repository: gcr.io/kubebuilder/kube-rbac-proxy
|
||||
tag: v0.15.0
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 5m
|
||||
memory: 64Mi
|
||||
manager:
|
||||
args:
|
||||
- --health-probe-bind-address=:8081
|
||||
- --metrics-bind-address=127.0.0.1:8080
|
||||
- --metrics-bind-address=:8443
|
||||
- --leader-elect
|
||||
- --health-probe-bind-address=:8081
|
||||
containerSecurityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
image:
|
||||
repository: infisical/kubernetes-operator
|
||||
tag: v0.9.5
|
||||
tag: v0.10.0
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
@@ -40,6 +20,8 @@ controllerManager:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
replicas: 1
|
||||
serviceAccount:
|
||||
annotations: {}
|
||||
@@ -50,7 +32,7 @@ metricsService:
|
||||
- name: https
|
||||
port: 8443
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
targetPort: 8443
|
||||
type: ClusterIP
|
||||
kubernetesClusterDomain: cluster.local
|
||||
scopedNamespace: ""
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.21 as builder
|
||||
FROM golang:1.24 AS builder
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -12,17 +12,16 @@ COPY go.sum go.sum
|
||||
RUN go mod download
|
||||
|
||||
# Copy the go source
|
||||
COPY main.go main.go
|
||||
COPY cmd/main.go cmd/main.go
|
||||
COPY api/ api/
|
||||
COPY controllers/ controllers/
|
||||
COPY packages/ packages/
|
||||
COPY internal/ internal/
|
||||
|
||||
# Build
|
||||
# the GOARCH has not a default value to allow the binary be built according to the host where the command
|
||||
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
|
||||
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
|
||||
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager main.go
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
|
||||
|
||||
# Use distroless as minimal base image to package the manager binary
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
|
@@ -1,8 +1,6 @@
|
||||
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= infisical/kubernetes-operator:latest
|
||||
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
|
||||
ENVTEST_K8S_VERSION = 1.25.0
|
||||
VERSION ?= latest
|
||||
IMG ?= infisical/kubernetes-operator:${VERSION} # ${VERSION} will be replaced by the version in the CI step
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
@@ -11,6 +9,12 @@ else
|
||||
GOBIN=$(shell go env GOBIN)
|
||||
endif
|
||||
|
||||
# CONTAINER_TOOL defines the container tool to be used for building images.
|
||||
# Be aware that the target commands are only tested with Docker which is
|
||||
# scaffolded by default. However, you might want to replace it to use other
|
||||
# tools. (i.e. podman)
|
||||
CONTAINER_TOOL ?= docker
|
||||
|
||||
# Setting SHELL to bash allows bash commands to be executed by recipes.
|
||||
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
|
||||
SHELL = /usr/bin/env bash -o pipefail
|
||||
@@ -21,26 +25,6 @@ all: build
|
||||
|
||||
##@ General
|
||||
|
||||
# The help target prints out all targets with their descriptions organized
|
||||
# beneath their categories. The categories are represented by '##@' and the
|
||||
# target descriptions by '##'. The awk commands is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
# More info on the usage of ANSI control characters for terminal formatting:
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||
# More info on the awk command:
|
||||
# http://linuxcommand.org/lc3_adv_awk.php
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
|
||||
# ## Chart - NOTE: change helper file to have 15 length for full name method
|
||||
# helm-chart:
|
||||
# $(KUSTOMIZE) build config/default | helmify ../helm-charts/secrets-operator
|
||||
|
||||
HELMIFY ?= $(LOCALBIN)/helmify
|
||||
|
||||
.PHONY: helmify
|
||||
@@ -52,7 +36,9 @@ legacy-helm: manifests kustomize helmify
|
||||
$(KUSTOMIZE) build config/default | $(HELMIFY) ../helm-charts/secrets-operator
|
||||
|
||||
helm: manifests kustomize helmify
|
||||
./scripts/generate-helm.sh
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
|
||||
./scripts/generate-helm.sh ${VERSION}
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=controller:latest # reset back
|
||||
|
||||
## Yaml for Kubectl
|
||||
kubectl-install: manifests kustomize
|
||||
@@ -60,6 +46,22 @@ kubectl-install: manifests kustomize
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
|
||||
$(KUSTOMIZE) build config/default > kubectl-install/install-secrets-operator.yaml
|
||||
|
||||
|
||||
# The help target prints out all targets with their descriptions organized
|
||||
# beneath their categories. The categories are represented by '##@' and the
|
||||
# target descriptions by '##'. The awk command is responsible for reading the
|
||||
# entire set of makefiles included in this invocation, looking for lines of the
|
||||
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||
# More info on the usage of ANSI control characters for terminal formatting:
|
||||
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||
# More info on the awk command:
|
||||
# http://linuxcommand.org/lc3_adv_awk.php
|
||||
|
||||
.PHONY: help
|
||||
help: ## Display this help.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Development
|
||||
|
||||
.PHONY: manifests
|
||||
@@ -79,47 +81,94 @@ vet: ## Run go vet against code.
|
||||
go vet ./...
|
||||
|
||||
.PHONY: test
|
||||
test: manifests generate fmt vet envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
|
||||
test: manifests generate fmt vet setup-envtest ## Run tests.
|
||||
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out
|
||||
|
||||
# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
|
||||
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
|
||||
# CertManager is installed by default; skip with:
|
||||
# - CERT_MANAGER_INSTALL_SKIP=true
|
||||
KIND_CLUSTER ?= infisical-operator-test-e2e
|
||||
|
||||
.PHONY: setup-test-e2e
|
||||
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
|
||||
@command -v $(KIND) >/dev/null 2>&1 || { \
|
||||
echo "Kind is not installed. Please install Kind manually."; \
|
||||
exit 1; \
|
||||
}
|
||||
@case "$$($(KIND) get clusters)" in \
|
||||
*"$(KIND_CLUSTER)"*) \
|
||||
echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
|
||||
*) \
|
||||
echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
|
||||
$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
|
||||
esac
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
|
||||
KIND_CLUSTER=$(KIND_CLUSTER) go test ./test/e2e/ -v -ginkgo.v
|
||||
$(MAKE) cleanup-test-e2e
|
||||
|
||||
.PHONY: cleanup-test-e2e
|
||||
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
|
||||
@$(KIND) delete cluster --name $(KIND_CLUSTER)
|
||||
|
||||
.PHONY: lint
|
||||
lint: golangci-lint ## Run golangci-lint linter
|
||||
$(GOLANGCI_LINT) run
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
|
||||
$(GOLANGCI_LINT) run --fix
|
||||
|
||||
.PHONY: lint-config
|
||||
lint-config: golangci-lint ## Verify golangci-lint linter configuration
|
||||
$(GOLANGCI_LINT) config verify
|
||||
|
||||
##@ Build
|
||||
|
||||
.PHONY: build
|
||||
build: manifests generate fmt vet ## Build manager binary.
|
||||
go build -o bin/manager main.go
|
||||
go build -o bin/manager cmd/main.go
|
||||
|
||||
.PHONY: run
|
||||
run: manifests generate fmt vet ## Run a controller from your host.
|
||||
go run ./main.go
|
||||
go run ./cmd/main.go
|
||||
|
||||
# If you wish built the manager image targeting other platforms you can use the --platform flag.
|
||||
# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it.
|
||||
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
|
||||
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
|
||||
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
.PHONY: docker-build
|
||||
docker-build: test ## Build docker image with the manager.
|
||||
docker build -t ${IMG} .
|
||||
docker-build: ## Build docker image with the manager.
|
||||
$(CONTAINER_TOOL) build -t ${IMG} .
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push: ## Push docker image with the manager.
|
||||
docker push ${IMG}
|
||||
$(CONTAINER_TOOL) push ${IMG}
|
||||
|
||||
# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple
|
||||
# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
|
||||
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
|
||||
# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/
|
||||
# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
|
||||
# To properly provided solutions that supports more than one platform you should use this option.
|
||||
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
|
||||
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
|
||||
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
|
||||
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
|
||||
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
|
||||
.PHONY: docker-buildx
|
||||
docker-buildx: test ## Build and push docker image for the manager for cross-platform support
|
||||
docker-buildx: ## Build and push docker image for the manager for cross-platform support
|
||||
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
|
||||
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
|
||||
- docker buildx create --name project-v3-builder
|
||||
docker buildx use project-v3-builder
|
||||
- docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
|
||||
- docker buildx rm project-v3-builder
|
||||
- $(CONTAINER_TOOL) buildx create --name infisical-operator-builder
|
||||
$(CONTAINER_TOOL) buildx use infisical-operator-builder
|
||||
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
|
||||
- $(CONTAINER_TOOL) buildx rm infisical-operator-builder
|
||||
rm Dockerfile.cross
|
||||
|
||||
.PHONY: build-installer
|
||||
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
|
||||
mkdir -p dist
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
|
||||
$(KUSTOMIZE) build config/default > dist/install.yaml
|
||||
|
||||
##@ Deployment
|
||||
|
||||
ifndef ignore-not-found
|
||||
@@ -128,22 +177,22 @@ endif
|
||||
|
||||
.PHONY: install
|
||||
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
|
||||
$(KUSTOMIZE) build config/crd | kubectl apply -f -
|
||||
$(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f -
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
$(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
$(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
.PHONY: deploy
|
||||
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
|
||||
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
|
||||
$(KUSTOMIZE) build config/default | kubectl apply -f -
|
||||
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
|
||||
|
||||
.PHONY: undeploy
|
||||
undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
$(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
|
||||
$(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
|
||||
|
||||
##@ Build Dependencies
|
||||
##@ Dependencies
|
||||
|
||||
## Location to install dependencies to
|
||||
LOCALBIN ?= $(shell pwd)/bin
|
||||
@@ -151,31 +200,62 @@ $(LOCALBIN):
|
||||
mkdir -p $(LOCALBIN)
|
||||
|
||||
## Tool Binaries
|
||||
KUBECTL ?= kubectl
|
||||
KIND ?= kind
|
||||
KUSTOMIZE ?= $(LOCALBIN)/kustomize
|
||||
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
|
||||
ENVTEST ?= $(LOCALBIN)/setup-envtest
|
||||
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
|
||||
|
||||
## Tool Versions
|
||||
KUSTOMIZE_VERSION ?= v3.8.7
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.10.0
|
||||
KUSTOMIZE_VERSION ?= v5.6.0
|
||||
CONTROLLER_TOOLS_VERSION ?= v0.18.0
|
||||
#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
|
||||
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
|
||||
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
|
||||
ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
|
||||
GOLANGCI_LINT_VERSION ?= v2.1.6
|
||||
|
||||
KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"
|
||||
.PHONY: kustomize
|
||||
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading.
|
||||
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
|
||||
$(KUSTOMIZE): $(LOCALBIN)
|
||||
@if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \
|
||||
echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \
|
||||
rm -rf $(LOCALBIN)/kustomize; \
|
||||
fi
|
||||
test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); }
|
||||
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
|
||||
|
||||
.PHONY: controller-gen
|
||||
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten.
|
||||
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
|
||||
$(CONTROLLER_GEN): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \
|
||||
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
|
||||
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
|
||||
|
||||
.PHONY: setup-envtest
|
||||
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
|
||||
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
|
||||
@$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \
|
||||
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
.PHONY: envtest
|
||||
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
|
||||
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
|
||||
$(ENVTEST): $(LOCALBIN)
|
||||
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
|
||||
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
|
||||
|
||||
.PHONY: golangci-lint
|
||||
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
|
||||
$(GOLANGCI_LINT): $(LOCALBIN)
|
||||
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
|
||||
|
||||
# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
|
||||
# $1 - target path with name of binary
|
||||
# $2 - package url which can be installed
|
||||
# $3 - specific version of package
|
||||
define go-install-tool
|
||||
@[ -f "$(1)-$(3)" ] || { \
|
||||
set -e; \
|
||||
package=$(2)@$(3) ;\
|
||||
echo "Downloading $${package}" ;\
|
||||
rm -f $(1) || true ;\
|
||||
GOBIN=$(LOCALBIN) go install $${package} ;\
|
||||
mv $(1) $(1)-$(3) ;\
|
||||
} ;\
|
||||
ln -sf $(1)-$(3) $(1)
|
||||
endef
|
||||
|
@@ -2,37 +2,38 @@
|
||||
# This file is used to track the info used to scaffold your project
|
||||
# and allow the plugins properly work.
|
||||
# More info: https://book.kubebuilder.io/reference/project-config.html
|
||||
cliVersion: 4.7.0
|
||||
domain: infisical.com
|
||||
layout:
|
||||
- go.kubebuilder.io/v3
|
||||
- go.kubebuilder.io/v4
|
||||
projectName: k8-operator
|
||||
repo: github.com/Infisical/infisical/k8-operator
|
||||
resources:
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: infisical.com
|
||||
group: secrets
|
||||
kind: InfisicalSecret
|
||||
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
|
||||
version: v1alpha1
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: infisical.com
|
||||
group: secrets
|
||||
kind: InfisicalPushSecretSecret
|
||||
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
|
||||
version: v1alpha1
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: infisical.com
|
||||
group: secrets
|
||||
kind: InfisicalDynamicSecret
|
||||
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
|
||||
version: v1alpha1
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: infisical.com
|
||||
group: secrets
|
||||
kind: InfisicalSecret
|
||||
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
|
||||
version: v1alpha1
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: infisical.com
|
||||
group: secrets
|
||||
kind: InfisicalPushSecretSecret
|
||||
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
|
||||
version: v1alpha1
|
||||
- api:
|
||||
crdVersion: v1
|
||||
namespaced: true
|
||||
controller: true
|
||||
domain: infisical.com
|
||||
group: secrets
|
||||
kind: InfisicalDynamicSecret
|
||||
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
|
||||
version: v1alpha1
|
||||
version: "3"
|
||||
|
@@ -1,8 +1,7 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2022.
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
285
k8-operator/cmd/main.go
Normal file
285
k8-operator/cmd/main.go
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/controller"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(secretsv1alpha1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var metricsCertPath, metricsCertName, metricsCertKey string
|
||||
var webhookCertPath, webhookCertName, webhookCertKey string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var namespace string
|
||||
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&namespace, "namespace", "", "Watch InfisicalSecrets scoped in the provided namespace only")
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
|
||||
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
|
||||
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
|
||||
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
|
||||
"The directory that contains the metrics server certificate.")
|
||||
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
|
||||
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
// Create watchers for metrics and webhooks certificates
|
||||
var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher
|
||||
|
||||
// Initial webhook TLS options
|
||||
webhookTLSOpts := tlsOpts
|
||||
|
||||
if len(webhookCertPath) > 0 {
|
||||
setupLog.Info("Initializing webhook certificate watcher using provided certificates",
|
||||
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
|
||||
|
||||
var err error
|
||||
webhookCertWatcher, err = certwatcher.New(
|
||||
filepath.Join(webhookCertPath, webhookCertName),
|
||||
filepath.Join(webhookCertPath, webhookCertKey),
|
||||
)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "Failed to initialize webhook certificate watcher")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) {
|
||||
config.GetCertificate = webhookCertWatcher.GetCertificate
|
||||
})
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: webhookTLSOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
}
|
||||
|
||||
// If the certificate is not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
//
|
||||
// TODO(user): If you enable certManager, uncomment the following lines:
|
||||
// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
|
||||
// managed by cert-manager for the metrics server.
|
||||
// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
|
||||
if len(metricsCertPath) > 0 {
|
||||
setupLog.Info("Initializing metrics certificate watcher using provided certificates",
|
||||
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
|
||||
|
||||
var err error
|
||||
metricsCertWatcher, err = certwatcher.New(
|
||||
filepath.Join(metricsCertPath, metricsCertName),
|
||||
filepath.Join(metricsCertPath, metricsCertKey),
|
||||
)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "to initialize metrics certificate watcher", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) {
|
||||
config.GetCertificate = metricsCertWatcher.GetCertificate
|
||||
})
|
||||
}
|
||||
|
||||
managerOptions := ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "cf2b8c44.infisical.com",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
}
|
||||
|
||||
// Only set cache options if we're namespace-scoped
|
||||
if namespace != "" {
|
||||
managerOptions.Cache = cache.Options{
|
||||
Scheme: scheme,
|
||||
DefaultNamespaces: map[string]cache.Config{
|
||||
namespace: {}, // whichever namespace the operator is running in
|
||||
},
|
||||
}
|
||||
ctrl.Log.Info(fmt.Sprintf("Watching CRDs in [namespace=%s]", namespace))
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), managerOptions)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := (&controller.InfisicalSecretReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
BaseLogger: ctrl.Log,
|
||||
Namespace: namespace,
|
||||
IsNamespaceScoped: namespace != "",
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "InfisicalSecret")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := (&controller.InfisicalPushSecretReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
IsNamespaceScoped: namespace != "",
|
||||
Namespace: namespace,
|
||||
BaseLogger: ctrl.Log,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "InfisicalPushSecret")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := (&controller.InfisicalDynamicSecretReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
BaseLogger: ctrl.Log,
|
||||
IsNamespaceScoped: namespace != "",
|
||||
Namespace: namespace,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "InfisicalDynamicSecret")
|
||||
os.Exit(1)
|
||||
}
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if metricsCertWatcher != nil {
|
||||
setupLog.Info("Adding metrics certificate watcher to manager")
|
||||
if err := mgr.Add(metricsCertWatcher); err != nil {
|
||||
setupLog.Error(err, "unable to add metrics certificate watcher to manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if webhookCertWatcher != nil {
|
||||
setupLog.Info("Adding webhook certificate watcher to manager")
|
||||
if err := mgr.Add(webhookCertWatcher); err != nil {
|
||||
setupLog.Error(err, "unable to add webhook certificate watcher to manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
creationTimestamp: null
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
name: clustergenerators.secrets.infisical.com
|
||||
spec:
|
||||
group: secrets.infisical.com
|
||||
@@ -21,14 +20,19 @@ spec:
|
||||
description: ClusterGenerator represents a cluster-wide generator
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -47,27 +51,29 @@ spec:
|
||||
description: set allowRepeat to true to allow repeating characters.
|
||||
type: boolean
|
||||
digits:
|
||||
description: digits specifies the number of digits in the
|
||||
generated password. If omitted it defaults to 25% of the
|
||||
length of the password
|
||||
description: |-
|
||||
digits specifies the number of digits in the generated
|
||||
password. If omitted it defaults to 25% of the length of the password
|
||||
type: integer
|
||||
length:
|
||||
default: 24
|
||||
description: Length of the password to be generated. Defaults
|
||||
to 24
|
||||
description: |-
|
||||
Length of the password to be generated.
|
||||
Defaults to 24
|
||||
type: integer
|
||||
noUpper:
|
||||
default: false
|
||||
description: Set noUpper to disable uppercase characters
|
||||
type: boolean
|
||||
symbolCharacters:
|
||||
description: symbolCharacters specifies the special characters
|
||||
that should be used in the generated password.
|
||||
description: |-
|
||||
symbolCharacters specifies the special characters that should be used
|
||||
in the generated password.
|
||||
type: string
|
||||
symbols:
|
||||
description: symbols specifies the number of symbol characters
|
||||
in the generated password. If omitted it defaults to 25%
|
||||
of the length of the password
|
||||
description: |-
|
||||
symbols specifies the number of symbol characters in the generated
|
||||
password. If omitted it defaults to 25% of the length of the password
|
||||
type: integer
|
||||
type: object
|
||||
uuidSpec:
|
||||
|
@@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
creationTimestamp: null
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
name: infisicaldynamicsecrets.secrets.infisical.com
|
||||
spec:
|
||||
group: secrets.infisical.com
|
||||
@@ -22,14 +21,19 @@ spec:
|
||||
API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -74,11 +78,9 @@ spec:
|
||||
kubernetesAuth:
|
||||
properties:
|
||||
autoCreateServiceAccountToken:
|
||||
description: Optionally automatically create a service account
|
||||
token for the configured service account. If this is set
|
||||
to `true`, the operator will automatically create a service
|
||||
account token for the configured service account. This field
|
||||
is recommended in most cases.
|
||||
description: |-
|
||||
Optionally automatically create a service account token for the configured service account.
|
||||
If this is set to `true`, the operator will automatically create a service account token for the configured service account. This field is recommended in most cases.
|
||||
type: boolean
|
||||
identityId:
|
||||
type: string
|
||||
@@ -169,12 +171,11 @@ spec:
|
||||
properties:
|
||||
creationPolicy:
|
||||
default: Orphan
|
||||
description: 'The Kubernetes Secret creation policy. Enum with
|
||||
values: ''Owner'', ''Orphan''. Owner creates the secret and
|
||||
sets .metadata.ownerReferences of the InfisicalSecret CRD that
|
||||
created it. Orphan will not set the secret owner. This will
|
||||
result in the secret being orphaned and not deleted when the
|
||||
resource is deleted.'
|
||||
description: |-
|
||||
The Kubernetes Secret creation policy.
|
||||
Enum with values: 'Owner', 'Orphan'.
|
||||
Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it.
|
||||
Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted.
|
||||
type: string
|
||||
secretName:
|
||||
description: The name of the Kubernetes Secret
|
||||
@@ -196,9 +197,9 @@ spec:
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the top
|
||||
level of your template. Secrets defined in the template
|
||||
will take precedence over the injected ones.
|
||||
description: |-
|
||||
This injects all retrieved secrets into the top level of your template.
|
||||
Secrets defined in the template will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
@@ -240,43 +241,35 @@ spec:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource. --- This struct is intended for direct
|
||||
use as an array at the field path .status.conditions. For example,
|
||||
\n type FooStatus struct{ // Represents the observations of a
|
||||
foo's current state. // Known .status.conditions.type are: \"Available\",
|
||||
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating
|
||||
details about the transition. This may be an empty string.
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers
|
||||
of specific condition types may define expected values and
|
||||
meanings for this field, and whether the values are considered
|
||||
a guaranteed API. The value should be a CamelCase string.
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
@@ -291,10 +284,6 @@ spec:
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
|
@@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.10.0
|
||||
creationTimestamp: null
|
||||
controller-gen.kubebuilder.io/version: v0.18.0
|
||||
name: infisicalpushsecrets.secrets.infisical.com
|
||||
spec:
|
||||
group: secrets.infisical.com
|
||||
@@ -22,14 +21,19 @@ spec:
|
||||
API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
@@ -74,11 +78,9 @@ spec:
|
||||
kubernetesAuth:
|
||||
properties:
|
||||
autoCreateServiceAccountToken:
|
||||
description: Optionally automatically create a service account
|
||||
token for the configured service account. If this is set
|
||||
to `true`, the operator will automatically create a service
|
||||
account token for the configured service account. This field
|
||||
is recommended in most cases.
|
||||
description: |-
|
||||
Optionally automatically create a service account token for the configured service account.
|
||||
If this is set to `true`, the operator will automatically create a service account token for the configured service account. This field is recommended in most cases.
|
||||
type: boolean
|
||||
identityId:
|
||||
type: string
|
||||
@@ -208,9 +210,9 @@ spec:
|
||||
description: The template key values
|
||||
type: object
|
||||
includeAllSecrets:
|
||||
description: This injects all retrieved secrets into the
|
||||
top level of your template. Secrets defined in the template
|
||||
will take precedence over the injected ones.
|
||||
description: |-
|
||||
This injects all retrieved secrets into the top level of your template.
|
||||
Secrets defined in the template will take precedence over the injected ones.
|
||||
type: boolean
|
||||
type: object
|
||||
required:
|
||||
@@ -253,43 +255,35 @@ spec:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: "Condition contains details for one aspect of the current
|
||||
state of this API Resource. --- This struct is intended for direct
|
||||
use as an array at the field path .status.conditions. For example,
|
||||
\n type FooStatus struct{ // Represents the observations of a
|
||||
foo's current state. // Known .status.conditions.type are: \"Available\",
|
||||
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: lastTransitionTime is the last time the condition
|
||||
transitioned from one status to another. This should be when
|
||||
the underlying condition changed. If that is not known, then
|
||||
using the time when the API field changed is acceptable.
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: message is a human readable message indicating
|
||||
details about the transition. This may be an empty string.
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: observedGeneration represents the .metadata.generation
|
||||
that the condition was set based upon. For instance, if .metadata.generation
|
||||
is currently 12, but the .status.conditions[x].observedGeneration
|
||||
is 9, the condition is out of date with respect to the current
|
||||
state of the instance.
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: reason contains a programmatic identifier indicating
|
||||
the reason for the condition's last transition. Producers
|
||||
of specific condition types may define expected values and
|
||||
meanings for this field, and whether the values are considered
|
||||
a guaranteed API. The value should be a CamelCase string.
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
@@ -304,10 +298,6 @@ spec:
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
--- Many .condition.type values are consistent across resources
|
||||
like Available, but because arbitrary conditions can be useful
|
||||
(see .node.status.conditions), the ability to deconflict is
|
||||
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user