mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' of https://github.com/Infisical/infisical into feat/laravel-forge-app-conn-and-secret-sync
This commit is contained in:
@@ -135,7 +135,9 @@ jobs:
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
echo "Checking for tag: $TAG_NAME"
|
||||
|
||||
if gh api repos/Infisical/infisical-omnibus/git/refs/tags/$TAG_NAME --silent 2>/dev/null; then
|
||||
EXACT_MATCH=$(gh api repos/Infisical/infisical-omnibus/git/refs/tags/$TAG_NAME | jq -r 'if type == "array" then .[].ref else .ref end' | grep -x "refs/tags/$TAG_NAME")
|
||||
|
||||
if [ "$EXACT_MATCH" == "refs/tags/$TAG_NAME" ]; then
|
||||
echo "Tag $TAG_NAME already exists, skipping..."
|
||||
else
|
||||
echo "Creating tag in Infisical/infisical-omnibus: $TAG_NAME"
|
||||
|
||||
@@ -6,7 +6,7 @@ ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||
FROM node:20.19.5-trixie-slim AS base
|
||||
|
||||
# Fixes NPM vulnerability: https://security.snyk.io/vuln/SNYK-JS-CROSSSPAWN-8303230
|
||||
RUN npm install -g npm@11
|
||||
RUN npm install -g npm@10.9.0
|
||||
|
||||
FROM base AS frontend-dependencies
|
||||
WORKDIR /app
|
||||
|
||||
@@ -6,7 +6,7 @@ ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||
FROM node:20.19.5-trixie-slim AS base
|
||||
|
||||
# Fixes NPM vulnerability: https://security.snyk.io/vuln/SNYK-JS-CROSSSPAWN-8303230
|
||||
RUN npm install -g npm@11
|
||||
RUN npm install -g npm@10.9.0
|
||||
|
||||
FROM base AS frontend-dependencies
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ export const initializeHsmModule = (envConfig: Pick<TEnvConfig, "isHsmConfigured
|
||||
|
||||
logger.info("PKCS#11 module initialized");
|
||||
} catch (error) {
|
||||
if (error instanceof pkcs11js.Pkcs11Error && error.code === pkcs11js.CKR_CRYPTOKI_ALREADY_INITIALIZED) {
|
||||
logger.error(error, "Failed to initialize PKCS#11 module");
|
||||
|
||||
if ((error as { message?: string })?.message === "CKR_CRYPTOKI_ALREADY_INITIALIZED") {
|
||||
logger.info("Skipping HSM initialization because it's already initialized.");
|
||||
} else {
|
||||
logger.error(error, "Failed to initialize PKCS#11 module");
|
||||
|
||||
@@ -44,7 +44,7 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
|
||||
// count org identities
|
||||
const identityDoc = await (tx || db.replicaNode())(TableName.Membership)
|
||||
.where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization })
|
||||
.where({ scope: AccessScope.Organization })
|
||||
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
|
||||
.where((bd) => {
|
||||
if (orgId) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AccessScope,
|
||||
OrganizationsSchema,
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
TableName,
|
||||
TMemberships,
|
||||
TMembershipsInsert,
|
||||
@@ -346,6 +347,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.replicaNode()(TableName.Membership)
|
||||
.where(`${TableName.Membership}.scopeOrgId`, orgId)
|
||||
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
|
||||
.where(`${TableName.Membership}.status`, OrgMembershipStatus.Accepted)
|
||||
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||
.count("*")
|
||||
.join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`)
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Contributing to the documentation
|
||||
|
||||
## Getting familiar with Mintlify
|
||||
New to Mintlify. [Start Here](https://mintlify.com/docs/quickstart)
|
||||
|
||||
New to Mintlify. [Start Here](https://mintlify.com/docs/quickstart)
|
||||
|
||||
## 👩💻 Development
|
||||
|
||||
Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command
|
||||
Install the [Mint CLI](https://www.npmjs.com/package/mint) to preview the documentation changes locally. To install, use the following command
|
||||
|
||||
```
|
||||
npm i -g mintlify
|
||||
npm i -g mint
|
||||
```
|
||||
|
||||
Run the following command at the root of your documentation (where mint.json is)
|
||||
|
||||
```
|
||||
mintlify dev
|
||||
mint dev
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
|
||||
- `mint dev` isn't running - Run `mint update` to update the Mint CLI.
|
||||
- Page loads as a 404 - Make sure you are running in a folder with `mint.json`. Check the `/docs` folder
|
||||
|
||||
@@ -43,23 +43,23 @@ docker compose -f docker-compose.dev.yml down
|
||||
|
||||
We use [Mintlify](https://mintlify.com/) for our docs.
|
||||
|
||||
#### Install Mintlify CLI.
|
||||
#### Install Mint CLI.
|
||||
|
||||
```bash
|
||||
npm i -g mintlify
|
||||
npm i -g mint
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
yarn global add mintlify
|
||||
yarn global add mint
|
||||
```
|
||||
|
||||
#### Running the docs
|
||||
|
||||
Go to `docs` directory and run `mintlify dev`. This will start up the docs on `localhost:3000`
|
||||
Go to `docs` directory and run `mint dev`. This will start up the docs on `localhost:3000`
|
||||
|
||||
```bash
|
||||
# From the root directory
|
||||
cd docs; mintlify dev;
|
||||
cd docs; mint dev;
|
||||
```
|
||||
|
||||
@@ -317,6 +317,7 @@
|
||||
"self-hosting/deployment-options/linux-upgrade"
|
||||
]
|
||||
},
|
||||
"self-hosting/guides/replication",
|
||||
"self-hosting/guides/upgrading-infisical",
|
||||
"self-hosting/configuration/envars",
|
||||
"self-hosting/guides/releases",
|
||||
|
||||
@@ -36,7 +36,6 @@ Enabling HSM encryption has a set of key benefits:
|
||||
|
||||
### Requirements
|
||||
- An Infisical instance with a version number that is equal to or greater than `v0.91.0`.
|
||||
- If you are using Docker, your instance must be using the `infisical/infisical-fips` image.
|
||||
- An HSM device from a provider such as [Thales Luna HSM](https://cpl.thalesgroup.com/encryption/data-protection-on-demand/services/luna-cloud-hsm), [AWS CloudHSM](https://aws.amazon.com/cloudhsm/), [Fortanix HSM](https://www.fortanix.com/platform/data-security-manager), or others.
|
||||
|
||||
|
||||
@@ -238,7 +237,7 @@ Enabling HSM encryption has a set of key benefits:
|
||||
-e DB_CONNECTION_URI="<>" \
|
||||
-e REDIS_URL="<>" \
|
||||
-e SITE_URL="<>" \
|
||||
infisical/infisical-fips:<version> # Replace <version> with the version you want to use
|
||||
infisical/infisical:<version> # Replace <version> with the version you want to use
|
||||
```
|
||||
|
||||
We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical).
|
||||
@@ -309,7 +308,7 @@ Enabling HSM encryption has a set of key benefits:
|
||||
-e DB_CONNECTION_URI="<>" \
|
||||
-e REDIS_URL="<>" \
|
||||
-e SITE_URL="<>" \
|
||||
infisical/infisical-fips:<version> # Replace <version> with the version you want to use
|
||||
infisical/infisical:<version> # Replace <version> with the version you want to use
|
||||
```
|
||||
|
||||
<Warning>
|
||||
@@ -319,6 +318,192 @@ Enabling HSM encryption has a set of key benefits:
|
||||
</Steps>
|
||||
After following these steps, your Docker setup will be ready to use Fortanix HSM encryption.
|
||||
</Tab>
|
||||
|
||||
|
||||
|
||||
<Tab title="AWS CloudHSM">
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- An [activated AWS CloudHSM cluster](https://docs.aws.amazon.com/cloudhsm/latest/userguide/activate-cluster.html) with at least 1 HSM device.
|
||||
- A [HSM user with the `Crypto User` role](https://docs.aws.amazon.com/cloudhsm/latest/userguide/cloudhsm_cli-user-create.html). In this guide we are using a user with the username `testUser` and the password `testPassword`.
|
||||
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Configure CloudHSM client">
|
||||
Before using the CloudHSM client, it must be configured properly so Infisical can use it for cryptographic operations.
|
||||
|
||||
|
||||
**1. Download the AWS CloudHSM client**
|
||||
|
||||
You can download the AWS CloudHSM client from [the AWS documentation](https://docs.aws.amazon.com/cloudhsm/latest/userguide/pkcs11-library-install.html).
|
||||
|
||||
<Note>
|
||||
Note that the AWS CloudHSM client is only available for Linux and Windows.
|
||||
If you're on a different operating system, you'll need to access a Linux machine to configure the client, such as an AWS EC2 Debian instance.
|
||||
</Note>
|
||||
|
||||
**2. Configure the CloudHSM client**
|
||||
|
||||
After installing the CloudHSM client, you should see all related files in the `/opt/cloudhsm/` directory on your machine.
|
||||
|
||||
You need to run the `configure-pkcs11` binary which will configure the client to connect with your AWS CloudHSM cluster. Depending on if you have multiple HSM's inside your cluster, you'll need to run the command with different arguments. Below you'll find the appropriate command for your use case:
|
||||
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Single HSM">
|
||||
|
||||
```bash
|
||||
sudo /opt/cloudhsm/bin/configure-pkcs11 -a <HSM_ENI_IPV4_ADDRESS> --disable-key-availability-check
|
||||
```
|
||||
|
||||
<Info>
|
||||
To use a single HSM, you must first manage client key durability settings by setting `disable_key_availability_check` to true by passing the `--disable-key-availability-check` flag. For more information read the [Key Synchronization](https://docs.aws.amazon.com/cloudhsm/latest/userguide/manage-key-sync.html) section in the AWS CloudHSM documentation.
|
||||
</Info>
|
||||
</Accordion>
|
||||
<Accordion title="Multiple HSM's">
|
||||
|
||||
```bash
|
||||
sudo /opt/cloudhsm/bin/configure-pkcs11 -a <HSM_ENI_IPV4_ADDRESS_1> <HSM_ENI_IPV4_ADDRESS_2> ... --disable-key-availability-check
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
At this point you should have:
|
||||
1. [Activated the CloudHSM cluster](https://docs.aws.amazon.com/cloudhsm/latest/userguide/activate-cluster.html)
|
||||
2. [Created a Crypto User HSM user](https://docs.aws.amazon.com/cloudhsm/latest/userguide/cloudhsm_cli-user-create.html)
|
||||
3. Downloaded and configured the CloudHSM client as described in the previous steps.
|
||||
|
||||
**3. Download the configured HSM client files**
|
||||
|
||||
After configuring the CloudHSM client, you should notice that the PKCS11 configuration file has been updated to include the HSM's ENI IP address. You can find this file in the `/opt/cloudhsm/etc/cloudhsm-pkcs11.cfg` directory, and it should look like this:
|
||||
|
||||
```json cloudhsm-pkcs11.cfg
|
||||
{
|
||||
"clusters": [
|
||||
{
|
||||
"type": "hsm1",
|
||||
"cluster": {
|
||||
// Your issuing CA certificate.
|
||||
// As per AWS documentation, this defaults to `/opt/cloudhsm/etc/customerCA.crt`.
|
||||
"hsm_ca_file": "/opt/cloudhsm/etc/customerCA.crt",
|
||||
"servers": [
|
||||
{
|
||||
"hostname": "<HSM_ENI_IPV4_ADDRESS_1>",
|
||||
"port": 2223,
|
||||
"enable": true
|
||||
},
|
||||
{
|
||||
"hostname": "<HSM_ENI_IPV4_ADDRESS_2>",
|
||||
"port": 2223,
|
||||
"enable": true
|
||||
}
|
||||
],
|
||||
// Only relevant if you passed the --disable-key-availability-check flag
|
||||
"options": {
|
||||
"disable_key_availability_check": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"log_type": "file",
|
||||
"log_file": "/opt/cloudhsm/run/cloudhsm-pkcs11.log",
|
||||
"log_level": "info",
|
||||
"log_interval": "daily"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Save the entire `/opt/cloudhsm` folder, as you will need to mount this to your Infisical Docker container in the later steps. In this guide we will be saving all the files from the folder as `/etc/cloudhsm` and mounting it to the `/etc/cloudhsm` directory in the Docker container.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Find HSM slot number">
|
||||
On the same machine that you configured the CloudHSM client, you can use `pkcs11-tool` to find the HSM slot number and to verify that the client is working correctly.
|
||||
|
||||
First, install the `pkcs11-tool` package:
|
||||
|
||||
```bash
|
||||
sudo apt-get install opensc -y
|
||||
```
|
||||
|
||||
Then, run the following command to find the HSM slot number:
|
||||
|
||||
```bash
|
||||
pkcs11-tool --module /opt/cloudhsm/lib/libcloudhsm_pkcs11.so --list-slots --login
|
||||
```
|
||||
|
||||
It'll prompt you to log in with your PIN, which is your username and password separated by a colon. Example: `testUser:testPassword`.
|
||||
|
||||
This will output the HSM slot number like so:
|
||||
|
||||
```bash
|
||||
ubuntu@ec-2:~$ pkcs11-tool --module /opt/cloudhsm/lib/libcloudhsm_pkcs11.so --list-slots
|
||||
Available slots:
|
||||
Slot 0 (0x2000000000000001): hsm1
|
||||
token label : hsm1
|
||||
token manufacturer : Marvell Semiconductors, Inc.
|
||||
token model : LS2
|
||||
token flags : login required, rng, token initialized
|
||||
hardware version : 66.48
|
||||
firmware version : 10.2
|
||||
serial num :
|
||||
pin min/max : 8/32
|
||||
```
|
||||
|
||||
In this case we see that the HSM has a slot in the position of `0`. This slot number will be used in the later steps to set the `HSM_SLOT` environment variable.
|
||||
</Step>
|
||||
|
||||
<Step title="Download the HSM issuing CA certificate">
|
||||
When you initialized your HSM, you were prompted to download the cluster CSR and sign it.
|
||||
In order to use the HSM with Infisical, you need to obtain the issuer CA certificate that was used to sign the cluster CSR.
|
||||
|
||||
If you followed [the official AWS documentation](https://docs.aws.amazon.com/cloudhsm/latest/userguide/initialize-cluster.html), you should have a CA certificate called `customerCA.crt`.
|
||||
|
||||
Save the CA certificate to a path, as this will need to be mounted as a Docker volume in the next step. For this example, we'll save it to `/aws-files/customerCA.crt`.
|
||||
</Step>
|
||||
|
||||
<Step title="Run Docker">
|
||||
Running Docker with HSM encryption requires setting the HSM-related environment variables as mentioned previously in the [HSM setup instructions](#setup-instructions). You can set these environment variables in your Docker run command.
|
||||
|
||||
We are setting the environment variables for Docker via the command line in this example, but you can also pass in a `.env` file to set these environment variables.
|
||||
|
||||
<Warning>
|
||||
If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||
Infisical depends on an AES and HMAC key to be present in the HSM. If these keys are not present, Infisical will create them. The AES key label will be the value of the `HSM_KEY_LABEL` environment variable, and the HMAC key label will be the value of the `HSM_KEY_LABEL` environment variable with the suffix `_HMAC`.
|
||||
</Warning>
|
||||
|
||||
```bash
|
||||
docker run -p 80:8080 \
|
||||
|
||||
# Mount the HSM client files to "/opt/cloudhsm"
|
||||
-v /etc/cloudhsm:/opt/cloudhsm \
|
||||
# Mount the issuer CA certificate to "/opt/cloudhsm/etc/customerCA.crt"
|
||||
-v /aws-files/customerCA.crt:/opt/cloudhsm/etc/customerCA.crt \
|
||||
|
||||
# Set the HSM library path to whats expected within Docker (/opt/cloudhsm/lib/libcloudhsm_pkcs11.so)
|
||||
-e HSM_LIB_PATH="/opt/cloudhsm/lib/libcloudhsm_pkcs11.so" \
|
||||
# Set the HSM PIN to the username and password of the HSM user, separated by a colon
|
||||
-e HSM_PIN=CryptoUserUsername:CryptoUserPassword \
|
||||
# Set the HSM slot number to the slot number of the HSM device as found in the previous step
|
||||
-e HSM_SLOT=<hsm-device-slot> \
|
||||
# Set the HSM key label to a label that will be used to identify the encryption key in the HSM. This key label does not need to exist before hand.
|
||||
-e HSM_KEY_LABEL=infisical-crypto-key \
|
||||
|
||||
# The rest of your environment variables ...
|
||||
# -e ...
|
||||
infisical/infisical:<version> # Replace <version> with the version you want to use
|
||||
```
|
||||
|
||||
We recommend reading further about [using Infisical with Docker](/self-hosting/deployment-options/standalone-infisical).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
After following these steps, your Docker setup will be ready to use HSM encryption.
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
</Tab>
|
||||
<Tab title="Kubernetes">
|
||||
@@ -326,8 +511,9 @@ Enabling HSM encryption has a set of key benefits:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Thales Luna Cloud HSM">
|
||||
|
||||
<Note>
|
||||
This is only supported on helm chart version `1.4.1` and above. Please see the [Helm Chart Changelog](https://github.com/Infisical/infisical/blob/main/helm-charts/infisical-standalone-postgres/CHANGELOG.md#141-march-19-2025) for more information.
|
||||
This is only supported on helm chart version `1.7.1` and above. Please see the [Helm Chart Changelog](https://github.com/Infisical/infisical/blob/main/helm-charts/infisical-standalone-postgres/CHANGELOG.md#141-march-19-2025) for more information.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
@@ -591,13 +777,11 @@ Enabling HSM encryption has a set of key benefits:
|
||||
<Step title="Updating the Deployment">
|
||||
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
|
||||
|
||||
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with HSM support.
|
||||
|
||||
```yaml
|
||||
# ... The rest of the values.yaml file ...
|
||||
|
||||
image:
|
||||
repository: infisical/infisical-fips # Very important: Must use "infisical/infisical-fips"
|
||||
repository: infisical/infisical
|
||||
tag: "v0.117.1-postgres"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
@@ -757,13 +941,13 @@ Enabling HSM encryption has a set of key benefits:
|
||||
</Step>
|
||||
|
||||
<Step title="Update Helm Values">
|
||||
Update your Helm values to use the FIPS-compliant image and mount the Fortanix HSM files:
|
||||
Update your Helm values to mount the Fortanix HSM files:
|
||||
|
||||
```yaml
|
||||
# ... The rest of the values.yaml file ...
|
||||
|
||||
image:
|
||||
repository: infisical/infisical-fips # Must use "infisical/infisical-fips"
|
||||
repository: infisical/infisical
|
||||
tag: "v0.117.1-postgres"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
@@ -800,6 +984,493 @@ Enabling HSM encryption has a set of key benefits:
|
||||
</Steps>
|
||||
After following these steps, your Kubernetes setup will be ready to use Fortanix HSM encryption.
|
||||
</Tab>
|
||||
|
||||
<Tab title="AWS CloudHSM">
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- An [activated AWS CloudHSM cluster](https://docs.aws.amazon.com/cloudhsm/latest/userguide/activate-cluster.html) with at least 1 HSM device.
|
||||
- A [HSM user with the `Crypto User` role](https://docs.aws.amazon.com/cloudhsm/latest/userguide/cloudhsm_cli-user-create.html). In this guide we are using a user with the username `testUser` and the password `testPassword`.
|
||||
- A Kubernetes cluster
|
||||
|
||||
<Note>
|
||||
AWS CloudHSM is supported on helm chart version `1.7.1` and above. Please see the [Helm Chart Changelog](https://github.com/Infisical/infisical/blob/main/helm-charts/infisical-standalone-postgres/CHANGELOG.md#141-march-19-2025) for more information.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Creating Persistent Volume Claim (PVC)">
|
||||
<Accordion title="Prerequisites for using AWS EKS">
|
||||
If you're using AWS EKS, you need to specify a storage class for the PVC and ensure that the EBS CSI Driver is installed and running.
|
||||
|
||||
By default, EKS exposes `gp2` as the default storage class. Below are the steps required for setting the default storage class and ensuring the EBS CSI Driver is installed and running:
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Enable OIDC authentication">
|
||||
|
||||
Enable OIDC authentication for the EKS cluster:
|
||||
```bash
|
||||
eksctl utils associate-iam-oidc-provider \
|
||||
--region <your-region> \
|
||||
--cluster <your-cluster-name> \
|
||||
--approve
|
||||
```
|
||||
|
||||
* Replace `<your-region>` with your AWS region.
|
||||
* Replace `<your-cluster-name>` with your cluster name.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Check if the EBS CSI Driver is installed and running">
|
||||
|
||||
1. Check if EBS CSI Driver is installed and running by running the following command:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system | grep ebs-csi
|
||||
```
|
||||
|
||||
If you see no pods, you need to install the EBS CSI Driver as seen in the next step.
|
||||
</Step>
|
||||
|
||||
<Step title="Install EBS CSI Driver using eksctl">
|
||||
Create a new IAM service account for the EBS CSI Driver:
|
||||
|
||||
```bash
|
||||
eksctl create iamserviceaccount \
|
||||
--name ebs-csi-controller-sa \
|
||||
--namespace kube-system \
|
||||
--region <your-region> \
|
||||
--cluster <your-cluster-name> \
|
||||
--attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
|
||||
--approve \
|
||||
--role-name AmazonEKS_EBS_CSI_DriverRole
|
||||
```
|
||||
|
||||
* Replace `<your-cluster-name>` with your cluster name.
|
||||
* Replace `<your-region>` with your AWS region.
|
||||
|
||||
Install the EBS CSI Driver:
|
||||
|
||||
```bash
|
||||
eksctl create addon \
|
||||
--name aws-ebs-csi-driver \
|
||||
--cluster <your-cluster-name> \
|
||||
--region <your-region> \
|
||||
--service-account-role-arn arn:aws:iam::<account-id>:role/AmazonEKS_EBS_CSI_DriverRole \
|
||||
--force
|
||||
```
|
||||
|
||||
* Replace `<your-cluster-name>` with your cluster name.
|
||||
* Replace `<your-region>` with your AWS region.
|
||||
* Replace `<account-id>` with your actual account ID. Can be obtained by running `aws sts get-caller-identity --query Account --output text`.
|
||||
</Step>
|
||||
|
||||
<Step title="Verify the EBS CSI Driver is installed and running">
|
||||
Verify the EBS CSI Driver is installed and running by running the following command:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system | grep ebs-csi
|
||||
```
|
||||
|
||||
You should see an output like this:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system | grep ebs-csi
|
||||
ebs-csi-controller-6b6bbf996-rvf8r 6/6 Running 0 21s
|
||||
ebs-csi-controller-6b6bbf996-vk4ng 6/6 Running 0 21s
|
||||
ebs-csi-node-c6vbb 3/3 Running 0 21s
|
||||
ebs-csi-node-s9zlr 3/3 Running 0 21s
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Find the enabled storage class">
|
||||
You can find the enabled storage class by running the following command:
|
||||
|
||||
```bash
|
||||
kubectl get storageclass
|
||||
```
|
||||
|
||||
You should see an output like this:
|
||||
|
||||
```bash
|
||||
$ kubectl get storageclass
|
||||
|
||||
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
|
||||
gp2 kubernetes.io/aws-ebs Delete WaitForFirstConsumer false 65m
|
||||
```
|
||||
|
||||
In this case, the enabled storage class is `gp2`.
|
||||
|
||||
</Step>
|
||||
<Step title="Set the default storage class">
|
||||
You can set the default PVC storage class by patching the storage class with the following command:
|
||||
|
||||
```bash
|
||||
kubectl patch storageclass gp2 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
|
||||
```
|
||||
|
||||
This will set the `gp2` storage class as the default storage class.
|
||||
|
||||
|
||||
Now when you run `kubectl get storageclass`, you should see that `gp2` is the default storage class.
|
||||
|
||||
```bash
|
||||
$ kubectl get storageclass
|
||||
|
||||
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
|
||||
gp2 (default) kubernetes.io/aws-ebs Delete WaitForFirstConsumer false 68m
|
||||
```
|
||||
|
||||
Notice the `(default)` next to the `gp2` storage class.
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
You need to create a Persistent Volume Claim (PVC) to mount the HSM client files to the Infisical deployment.
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: cloudhsm-data-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 500Mi
|
||||
EOF
|
||||
```
|
||||
The above command will create a PVC named `cloudhsm-data-pvc` with a storage size of `500Mi`. You can change the storage size if needed.
|
||||
|
||||
Next we need to create a temporary pod with the PVC mounted as a volume, allowing us to copy the HSM client files into this mounted storage.
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: cloudhsm-setup-pod
|
||||
spec:
|
||||
containers:
|
||||
- name: setup
|
||||
image: debian:bookworm
|
||||
command: ["/bin/sh", "-c", "sleep 7200"]
|
||||
volumeMounts:
|
||||
- name: cloudhsm-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: cloudhsm-data
|
||||
persistentVolumeClaim:
|
||||
claimName: cloudhsm-data-pvc
|
||||
EOF
|
||||
```
|
||||
|
||||
The above command will create a pod named `cloudhsm-setup-pod` with a Debian image. The pod will sleep for 7200 seconds _(two hours)_, which is enough time to set up the PVC and configure the HSM client.
|
||||
|
||||
Ensure that the pod is running and is healthy by running the following command:
|
||||
|
||||
```bash
|
||||
kubectl wait --for=condition=Ready pod/cloudhsm-setup-pod --timeout=120s
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Configure the PVC">
|
||||
|
||||
We need to configure the PVC to work with the CloudHSM, so Infisical can consume the HSM client files.
|
||||
|
||||
**2.1. Start a shell in the PVC pod:**
|
||||
|
||||
This will allow us to run commands directly within the setup pod. We'll use this to configure the CloudHSM client and to validate that it's working correctly.
|
||||
|
||||
```bash
|
||||
kubectl exec -it cloudhsm-setup-pod -- /bin/sh
|
||||
```
|
||||
|
||||
**2.2. Install the necessary packages:**
|
||||
|
||||
This will install the necessary packages to allow us to test and install the CloudHSM client.
|
||||
|
||||
```bash
|
||||
apt-get update -y
|
||||
apt-get install opensc telnet wget -y
|
||||
```
|
||||
|
||||
**2.3. Try to reach the HSM device:**
|
||||
|
||||
We need to validate that we're able to reach the HSM device from within Kubernetes. You can use telnet to ping the HSM device like so:
|
||||
|
||||
```bash
|
||||
telnet <HSM_ENI_IPV4_ADDRESS> 2223
|
||||
```
|
||||
|
||||
You should see an output like this:
|
||||
```bash
|
||||
$ telnet <HSM_ENI_IPV4_ADDRESS> 2223
|
||||
Trying <HSM_ENI_IPV4_ADDRESS>...
|
||||
Connected to <HSM_ENI_IPV4_ADDRESS>.
|
||||
```
|
||||
|
||||
If it gets stuck on `Trying ....`, you may have configured your HSM client's security group incorrectly. Make sure you configure the security group to allow traffic from EKS on port 2223-2225.
|
||||
|
||||
**2.4. Install the AWS CloudHSM client:**
|
||||
|
||||
The Infisical images run on Debian, so we need to install a Debian-compatible version of the AWS CloudHSM client.
|
||||
|
||||
```bash
|
||||
wget https://s3.amazonaws.com/cloudhsmv2-software/CloudHsmClient/Jammy/cloudhsm-pkcs11_latest_u22.04_amd64.deb
|
||||
apt-get install ./cloudhsm-pkcs11_latest_u22.04_amd64.deb -y
|
||||
```
|
||||
|
||||
**2.5. Configure the CloudHSM client:**
|
||||
|
||||
After installing the CloudHSM client, you should see all related files in the `/opt/cloudhsm/` directory on the CloudHSM setup pod.
|
||||
|
||||
You need to run the `configure-pkcs11` binary which will configure the client to connect with your AWS CloudHSM cluster. Depending on if you have multiple HSM's inside your cluster, you'll need to run the command with different arguments. Below you'll find the appropriate command for your use case:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Single HSM">
|
||||
|
||||
```bash
|
||||
/opt/cloudhsm/bin/configure-pkcs11 -a <HSM_ENI_IPV4_ADDRESS> --disable-key-availability-check
|
||||
```
|
||||
|
||||
<Info>
|
||||
To use a single HSM, you must first manage client key durability settings by setting `disable_key_availability_check` to true by passing the `--disable-key-availability-check` flag. For more information read the [Key Synchronization](https://docs.aws.amazon.com/cloudhsm/latest/userguide/manage-key-sync.html) section in the AWS CloudHSM documentation.
|
||||
</Info>
|
||||
</Accordion>
|
||||
<Accordion title="Multiple HSM's">
|
||||
|
||||
```bash
|
||||
/opt/cloudhsm/bin/configure-pkcs11 -a <HSM_ENI_IPV4_ADDRESS_1> <HSM_ENI_IPV4_ADDRESS_2> ... --disable-key-availability-check
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
**2.6. Verify the CloudHSM client is configured correctly:**
|
||||
|
||||
You can verify the CloudHSM client is configured correctly by running the following command:
|
||||
```bash
|
||||
cat /opt/cloudhsm/etc/cloudhsm-pkcs11.cfg
|
||||
```
|
||||
|
||||
You should see an output like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"clusters": [
|
||||
{
|
||||
"type": "hsm1",
|
||||
"cluster": {
|
||||
"hsm_ca_file": "/opt/cloudhsm/etc/customerCA.crt",
|
||||
"servers": [
|
||||
{
|
||||
"hostname": "172.31.39.155",
|
||||
"port": 2223,
|
||||
"enable": true
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"disable_key_availability_check": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"log_type": "file",
|
||||
"log_file": "/opt/cloudhsm/run/cloudhsm-pkcs11.log",
|
||||
"log_level": "info",
|
||||
"log_interval": "daily"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2.7. Exit the pod:**
|
||||
|
||||
Exit the pod by running the following command:
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
|
||||
**2.8. Copy your issuer CA certificate to the PVC:**
|
||||
|
||||
When you initialized your HSM, you were prompted to download the cluster CSR and sign it.
|
||||
In order to use the HSM with Infisical, you need to obtain the issuer CA certificate that was used to sign the cluster CSR.
|
||||
|
||||
If you followed [the official AWS documentation](https://docs.aws.amazon.com/cloudhsm/latest/userguide/initialize-cluster.html), you should have a CA certificate called `customerCA.crt`.
|
||||
|
||||
Copy the CA certificate from your local machine to the setup pod:
|
||||
|
||||
```bash
|
||||
kubectl cp /path/to/customerCA.crt cloudhsm-setup-pod:/opt/cloudhsm/etc/customerCA.crt
|
||||
```
|
||||
|
||||
Ensure that the file is at `/opt/cloudhsm/etc/customerCA.crt` inside the setup pod by running the following command:
|
||||
```bash
|
||||
kubectl exec -it cloudhsm-setup-pod -- cat /opt/cloudhsm/etc/customerCA.crt
|
||||
```
|
||||
|
||||
**2.9. Test the HSM client:**
|
||||
|
||||
Finally, after we're done configuring the HSM client, we need to test it to ensure that it's working correctly.
|
||||
|
||||
First, start a new shell into the setup pod by running the same shell command as before:
|
||||
```bash
|
||||
kubectl exec -it cloudhsm-setup-pod -- /bin/sh
|
||||
```
|
||||
|
||||
Next, try generating a random 32 bytes long string by running the following command:
|
||||
```bash
|
||||
pkcs11-tool --module /opt/cloudhsm/lib/libcloudhsm_pkcs11.so \
|
||||
--login --pin <crypto-user-username>:<crypto-user-password> \
|
||||
--generate-random 32 | base64
|
||||
```
|
||||
|
||||
You should see an output like this:
|
||||
```bash
|
||||
Using slot 0 with a present token (0x2000000000000001)
|
||||
av1dlhVEsssjpcTNS+ysGUoKWH6+/PCaEDIdal5oQc0=
|
||||
```
|
||||
|
||||
<Note>
|
||||
Replace the `<crypto-user-username>:<crypto-user-password>` with your username and password combination of the Crypto user you have created that you want to use to perform cryptographic operations.
|
||||
|
||||
In AWS CloudHSM, the PIN is always the username and password separated by a colon.
|
||||
</Note>
|
||||
|
||||
|
||||
|
||||
**2.10. Copy the configured client to the PVC:**
|
||||
|
||||
Copy from the HSM files into the `/data` directory in the PVC, which is what will be mounted for the Infisical deployment.
|
||||
```bash
|
||||
cp -r /opt/cloudhsm/. /data/
|
||||
```
|
||||
|
||||
Verify the files were copied correctly by running the following command:
|
||||
```bash
|
||||
ls -la /data/
|
||||
```
|
||||
|
||||
You should see an output like this:
|
||||
```bash
|
||||
drwxr-xr-x. 8 root root 4096 Oct 13 18:50 .
|
||||
drwxr-xr-x. 1 root root 131 Oct 13 18:29 ..
|
||||
drwxr-xr-x. 2 root root 4096 Oct 13 18:50 bin
|
||||
drwxr-xr-x. 3 root root 4096 Oct 13 18:50 doc
|
||||
drwxr-xr-x. 2 root root 4096 Oct 13 18:50 etc
|
||||
drwxr-xr-x. 3 root root 4096 Oct 13 18:50 include
|
||||
drwxr-xr-x. 2 root root 4096 Oct 13 18:50 lib
|
||||
drwxr-xr-t. 2 root root 4096 Oct 13 18:50 run
|
||||
```
|
||||
|
||||
**2.11. Set the correct permissions for the HSM client files:**
|
||||
|
||||
```bash
|
||||
chmod -R 755 /data/
|
||||
```
|
||||
|
||||
**2.12. Exit the pod:**
|
||||
|
||||
Exit the pod by running the following command:
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
|
||||
**2.13. Delete the setup pod:**
|
||||
|
||||
Delete the setup pod by running the following command:
|
||||
```bash
|
||||
kubectl delete pod cloudhsm-setup-pod
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Updating your environment variables">
|
||||
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
|
||||
We need to update the secret with the following environment variables:
|
||||
|
||||
- `HSM_LIB_PATH` - The path to the CloudHSM PKCS#11 library _(mapped to `/opt/cloudhsm/lib/libcloudhsm_pkcs11.so`)_
|
||||
- `HSM_PIN` - The PIN for the HSM device, which is the username and password of your Crypto User separated by a colon (e.g., `testUser:testPassword`)
|
||||
- `HSM_SLOT` - The slot number for the HSM device that you found in the previous step
|
||||
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||
|
||||
The following is an example of the secret that you should update:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: infisical-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
# ... Other environment variables ...
|
||||
HSM_LIB_PATH: "/opt/cloudhsm/lib/libcloudhsm_pkcs11.so"
|
||||
HSM_PIN: "testUser:testPassword" # Replace with your actual Crypto User credentials
|
||||
HSM_SLOT: "0" # Replace with your actual slot number
|
||||
HSM_KEY_LABEL: "infisical-crypto-key"
|
||||
```
|
||||
|
||||
Save the file after updating the environment variables, and apply the secret changes
|
||||
|
||||
```bash
|
||||
kubectl apply -f ./secret-file-name.yaml
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Updating the Deployment">
|
||||
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
|
||||
|
||||
```yaml
|
||||
# ... The rest of the values.yaml file ...
|
||||
infisical:
|
||||
image:
|
||||
repository: infisical/infisical
|
||||
tag: "v0.151.0-nightly-20251013.1"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
extraVolumeMounts:
|
||||
- name: cloudhsm-data
|
||||
mountPath: /opt/cloudhsm # The path we will mount the HSM client files to
|
||||
|
||||
extraVolumes:
|
||||
- name: cloudhsm-data
|
||||
persistentVolumeClaim:
|
||||
claimName: cloudhsm-data-pvc # The PVC we created in the previous step
|
||||
|
||||
# ... The rest of the values.yaml file ...
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Make sure to set the `tag` to **`v0.151.0-nightly-20251013.1` or above**, as this is the minimum Infisical version that supports AWS CloudHSM.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
Ensure that the configuration file at `/opt/cloudhsm/etc/cloudhsm-pkcs11.cfg` references the correct path for the issuer CA certificate (`/opt/cloudhsm/etc/customerCA.crt`). This should already be configured correctly if you followed the previous steps.
|
||||
</Warning>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Upgrading the Helm Chart">
|
||||
After updating the values.yaml file, you need to upgrade the Helm chart in order for the changes to take effect.
|
||||
|
||||
```bash
|
||||
helm repo update
|
||||
helm upgrade --install infisical infisical-helm-charts/infisical-standalone --values /path/to/values.yaml
|
||||
```
|
||||
</Step>
|
||||
<Step title="Restarting the Deployment">
|
||||
After upgrading the Helm chart, you need to restart the deployment in order for the changes to take effect.
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment/infisical-infisical-standalone-infisical
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
After following these steps, your Kubernetes setup will be ready to use AWS CloudHSM encryption.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
162
docs/self-hosting/guides/replication.mdx
Normal file
162
docs/self-hosting/guides/replication.mdx
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
|
||||
title: "Replication"
|
||||
description: "Learn how Infisical supports multi-region replication"
|
||||
|
||||
---
|
||||
|
||||
<Info>
|
||||
Infisical replication 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.
|
||||
</Info>
|
||||
|
||||
Multi-region replication is available in Infisical Enterprise to support globally distributed deployments. Understanding the architecture, use cases, and operational considerations is essential before implementing this feature in production environments.
|
||||
|
||||
Infisical uses a primary/secondary (1:N) architecture with asynchronous PostgreSQL replication. This design prioritizes high availability and minimal read latency for applications deployed across multiple geographic regions.
|
||||
|
||||
## Use cases
|
||||
|
||||
- **Multi-Region Deployments**: Serving secrets to applications distributed across continents from a single region introduces unacceptable latency. A centralized deployment also creates a single point of failure: regional outages can render secrets inaccessible globally, and network connectivity issues impact availability.
|
||||
|
||||
- **Geographic Data Locality**: Global organizations need to minimize the time it takes for applications to retrieve secrets and configurations. Regional replicas enable applications to fetch data from nearby instances rather than making cross-continental requests.
|
||||
|
||||
- **Disaster Recovery**: Organizations need resilience against primary region failures. Secondary regions with read replicas can be promoted to primary status when needed, maintaining operations during outages or disasters.
|
||||
|
||||
## Design Goals
|
||||
|
||||
In order to address the common use cases, the implementation reflects several core goals:
|
||||
|
||||
- **Optimized Read Performance**: Applications need fast access to secrets regardless of their location. Regional instances use Redis for aggressive caching and read from local PostgreSQL replicas, eliminating cross-region round trips for most read operations.
|
||||
|
||||
- **Conflict-Free Architecture**: All mutations flow through the primary instance exclusively. This prevents write conflicts and split-brain scenarios that plague multi-master systems. The trade-off ensures data integrity without requiring conflict resolution strategies.
|
||||
|
||||
- **Zero Client Changes**: Existing Infisical integrations, SDKs, and CLI tools work without modification. Regional instances route write operations to the primary while handling reads locally. Authentication tokens and API keys function identically across all instances.
|
||||
|
||||
- **Operational Simplicity**: Deploying additional regions requires minimal configuration. PostgreSQL handles replication complexity, and the stateless application tier scales horizontally without coordination overhead.
|
||||
|
||||
# Architecture
|
||||
|
||||
Infisical distinguishes between _primary_ and _secondary_ instances. The primary holds write authority and is the sole instance permitted to modify the PostgreSQL database. Secondary instances handle read traffic locally and proxy write operations to the primary.
|
||||
|
||||
## Infrastructure components
|
||||
|
||||
Two data stores form Infisical's persistence layer:
|
||||
|
||||
- **PostgreSQL** maintains the authoritative dataset including secrets with their version history, authentication credentials, user identities, project configurations, access policies, audit trails, and integration settings. All persistent state lives in PostgreSQL.
|
||||
|
||||
- **Redis** accelerates read operations through caching and manages asynchronous job queues. Each regional deployment maintains an independent Redis instance optimized for local access patterns.
|
||||
|
||||
The Infisical application servers are stateless and therefore hold no persistent data internally. This design simplifies regional deployment and horizontal scaling.
|
||||
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Primary region configuration">
|
||||
A primary deployment consists of three core components:
|
||||
|
||||
- **Application Servers**: Process all API requests directly, handling both read and write operations without forwarding
|
||||
- **PostgreSQL Primary Database**: Accepts read and write queries, serving as the authoritative source of truth
|
||||
- **Redis Cache**: Stores frequently accessed data and executes all background jobs including secret synchronization, scheduled tasks, and audit log processing
|
||||
|
||||
</Tab>
|
||||
<Tab title="Secondary region configuration">
|
||||
Each secondary deployment mirrors the primary structure with key differences:
|
||||
|
||||
- **Application Servers**: Service read requests from local infrastructure but forward any write requests to the primary region
|
||||
- **PostgreSQL Read Replica**: Continuously streams changes from the primary database via PostgreSQL replication
|
||||
- **PostgreSQL Primary Database**: Connection string to the primary database for write forwarding
|
||||
- **Redis Cache**: Maintains a local cache but processes only audit logs (other background jobs remain disabled)
|
||||
|
||||
Configuring a secondary region requires four main environment variables:
|
||||
|
||||
1. `INFISICAL_PRIMARY_INSTANCE_URL`: The primary region's Infisical API endpoint
|
||||
2. Postgres primary instance connection details. View related [environment variables](/self-hosting/configuration/envars#postgresql).
|
||||
3. Postgres read replica connection details. View related [environment variables](/self-hosting/configuration/envars#postgresql).
|
||||
4. Redis connection details. View related [environment variables](/self-hosting/configuration/envars#redis).
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## How requests are processed
|
||||
|
||||
When a client sends a read request to a secondary instance, the application first checks the local Redis cache for the requested data. If the data exists in cache, it's returned immediately to the client. Otherwise, the application queries the local PostgreSQL read replica, caches the result in Redis for future requests, and returns the response to the client.
|
||||
|
||||
Write operations follow a different path. When a secondary receives a write request, it forwards the complete request to the primary instance URL. The primary processes the mutation against the authoritative database and returns a response, which the secondary then forwards back to the client. PostgreSQL subsequently streams these changes to all replicas asynchronously.
|
||||
|
||||
Operations against the primary instance are more straightforward, as both reads and writes execute directly against local infrastructure without any forwarding.
|
||||
|
||||
## Replication mechanism
|
||||
|
||||
PostgreSQL streaming replication handles all data synchronization. When transactions commit on the primary, changes are written to the write-ahead log (WAL) and streamed to all configured replicas, which apply the entries to maintain consistency. Replication lag typically remains under one second.
|
||||
|
||||
This approach replicates all data stored in PostgreSQL: secrets and their version histories, user accounts and permissions, authentication tokens, project configurations, access policies, audit logs, integration settings, and all other application metadata. Replicas are eventually consistent. This means that all replicas eventually converge to the same state, typically under 1 second. The application layer remains unaware of replication mechanics and operates identically across all instances.
|
||||
|
||||
## Caching behavior
|
||||
|
||||
Redis caches are regional and independent (no coordination occurs between instances):
|
||||
|
||||
- Secondary instances populate caches on demand from read requests
|
||||
- Cache hits serve data without touching PostgreSQL
|
||||
- Cache misses fetch from the local replica and populate the cache
|
||||
- Each region maintains its own hot dataset based on local access patterns
|
||||
|
||||
Secrets use versioned caching. When a secret changes, its version identifier changes, causing automatic cache misses. This ensures subsequent reads fetch the updated value from PostgreSQL without requiring active cache invalidation.
|
||||
|
||||
# Technical Details
|
||||
|
||||
Understanding the implementation details can help evaluate whether Infisical's replication characteristics align with your requirements.
|
||||
The following sections provide deeper insight into performance behavior, failure modes, and the underlying mechanisms that drive the replication system.
|
||||
|
||||
### PostgreSQL streaming replication
|
||||
|
||||
Infisical relies on PostgreSQL's native replication, which provides:
|
||||
|
||||
- **Asynchronous operation**: The primary commits transactions immediately without waiting for replicas to confirm receipt. Replicas receive and apply changes continuously with typical lag measured in milliseconds to low seconds, depending on network conditions and write volume.
|
||||
|
||||
- **Binary-level consistency**: Replication occurs at the storage layer using write-ahead logs, guaranteeing replicas are byte-for-byte identical to the primary at the block level.
|
||||
|
||||
- **Promotion capability**: Read replicas can be promoted to primary during disaster recovery. Promotion requires updating Infisical configuration to designate the promoted instance as primary and reconfiguring other secondaries.
|
||||
|
||||
Consult PostgreSQL's official documentation for replication setup instructions specific to your hosting environment (RDS, Cloud SQL, self-managed, etc.).
|
||||
|
||||
### Version management
|
||||
|
||||
All Infisical instances must run identical versions (mixing versions risks database schema mismatches or incompatible API behavior). Database migrations execute only on the primary and replicate to secondaries through standard PostgreSQL mechanisms.
|
||||
|
||||
During upgrades:
|
||||
1. Upgrade the primary instance (migrations run automatically)
|
||||
2. Upgrade secondary instances to match
|
||||
3. All instances can continue running during the upgrade process since database migrations don't immediately drop tables/columns
|
||||
|
||||
### Request proxying
|
||||
|
||||
When a secondary receives a mutation request (POST, PUT, PATCH, DELETE), it functions as a transparent proxy:
|
||||
|
||||
1. Preserve the original request completely (headers, authentication context, request body)
|
||||
2. Forward to the primary instance URL specified in configuration
|
||||
3. Primary processes the request as a direct client request
|
||||
4. Return the primary's response unmodified to the client
|
||||
|
||||
### Cache management
|
||||
|
||||
Infisical uses versioned caching rather than active invalidation:
|
||||
|
||||
1. Secrets and other cached entities include version identifiers
|
||||
2. When data mutates, its version changes in the database
|
||||
3. Cache lookups include the version in the cache key
|
||||
4. Version changes cause automatic cache misses
|
||||
5. Cache misses fetch updated data from PostgreSQL
|
||||
6. Fresh data populates the cache with the new version
|
||||
|
||||
This strategy ensures correctness without requiring cross-region cache invalidation protocols.
|
||||
|
||||
### Background job processing
|
||||
|
||||
Secondary instances run with restricted background job capabilities:
|
||||
|
||||
**Active**: Audit log processing
|
||||
**Disabled**: Secret synchronization to third-party systems, scheduled tasks, cron jobs, time-triggered operations
|
||||
|
||||
Limiting background jobs to the primary prevents duplicate processing and ensures integrations execute once.
|
||||
|
||||
@@ -270,7 +270,7 @@
|
||||
"description": "This page shows the members of the selected project, and allows you to modify their permissions."
|
||||
},
|
||||
"org": {
|
||||
"title": "Organization Settings",
|
||||
"title": "Settings",
|
||||
"description": "Manage members of your organization. These users could afterwards be formed into projects."
|
||||
},
|
||||
"personal": {
|
||||
@@ -290,7 +290,7 @@
|
||||
}
|
||||
},
|
||||
"project": {
|
||||
"title": "Project Settings",
|
||||
"title": "Settings",
|
||||
"description": "These settings only apply to the currently selected Project.",
|
||||
"danger-zone": "Danger Zone",
|
||||
"delete-project": "Delete Project",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
import { useCreateWsTag } from "@app/hooks/api";
|
||||
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
export const secretTagsColors = [
|
||||
@@ -85,6 +86,8 @@ const isValidHexColor = (hexColor: string) => {
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
append: (data: WsTag) => void;
|
||||
currentSecret?: SecretV3RawSanitized;
|
||||
};
|
||||
|
||||
const createTagSchema = z.object({
|
||||
@@ -100,7 +103,7 @@ type TagColor = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
export const CreateTagModal = ({ isOpen, onToggle, append, currentSecret }: Props): JSX.Element => {
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
@@ -128,11 +131,12 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
|
||||
const onFormSubmit = async ({ slug, color }: FormData) => {
|
||||
try {
|
||||
await createWsTag({
|
||||
const data = await createWsTag({
|
||||
projectId,
|
||||
tagColor: color,
|
||||
tagSlug: slug
|
||||
});
|
||||
append(data);
|
||||
onToggle(false);
|
||||
reset();
|
||||
createNotification({
|
||||
@@ -151,8 +155,12 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent
|
||||
title="Create tag"
|
||||
subTitle="Specify your tag name, and the slug will be created automatically."
|
||||
title={currentSecret ? `Create tag for ${currentSecret.key}` : "Create tag"}
|
||||
subTitle={
|
||||
currentSecret
|
||||
? `Create a new tag, and it will be automatically linked to secret: ${currentSecret.key}.`
|
||||
: "Specify your tag name, and the slug will be created automatically."
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
@@ -253,7 +261,7 @@ export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Create
|
||||
{currentSecret ? "Create and Add" : "Create"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
|
||||
@@ -17,7 +17,14 @@ const badgeVariants = cva(
|
||||
variant: {
|
||||
primary: "bg-yellow/20 text-yellow",
|
||||
danger: "bg-red/20 text-red",
|
||||
success: "bg-green/20 text-green"
|
||||
success: "bg-green/20 text-green",
|
||||
org: "bg-org-v1/20 text-org-v1 [&_svg]:text-org-v1 flex items-center opacity-100 hover:bg-org-v1/10 [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
|
||||
namespace:
|
||||
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1 w-min whitespace-nowrap",
|
||||
project:
|
||||
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
|
||||
instance:
|
||||
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,16 +33,21 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
variant,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||
}: MenuItemProps<T> &
|
||||
ComponentPropsWithRef<T> & { variant?: "project" | "namespace" | "org" }): JSX.Element => {
|
||||
return (
|
||||
<Item
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={twMerge(
|
||||
"group relative mt-0.5 flex w-full cursor-pointer items-center rounded-sm px-2 py-2 font-inter text-sm text-bunker-100 transition-all duration-50 hover:bg-mineshaft-700",
|
||||
"group relative mt-0.5 box-border flex w-full cursor-pointer items-center rounded-[2px] border-l-2 border-transparent px-2 py-2 font-inter text-sm text-bunker-100 transition-all duration-50 hover:bg-mineshaft-700",
|
||||
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
isSelected && variant === "org" && "border-org-v1",
|
||||
isSelected && variant === "namespace" && "border-namespace-v1",
|
||||
isSelected && variant === "project" && "border-primary",
|
||||
className
|
||||
)}
|
||||
ref={inputRef}
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCube, faCubes, faGlobe, faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ReactNode } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Badge } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
scope: "org" | "project" | "namespace" | "instance";
|
||||
};
|
||||
|
||||
export const PageHeader = ({ title, description, children, className }: Props) => (
|
||||
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = {
|
||||
org: { label: "Organization", icon: faGlobe },
|
||||
project: { label: "Project", icon: faCube },
|
||||
namespace: { label: "Namespace", icon: faCubes },
|
||||
instance: { label: "Server", icon: faServer }
|
||||
};
|
||||
|
||||
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
||||
<div className={twMerge("mb-4 w-full", className)}>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="w-full">
|
||||
<h1 className="mr-4 text-3xl font-medium text-white capitalize">{title}</h1>
|
||||
<div className="mr-4 flex w-full items-center">
|
||||
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
|
||||
{scope && (
|
||||
<Badge variant={scope} className="mt-1 ml-2.5">
|
||||
<FontAwesomeIcon icon={SCOPE_NAME[scope].icon} />
|
||||
{SCOPE_NAME[scope].label}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -22,10 +22,18 @@ export const TabList = ({ className, children, ...props }: TabListProps) => (
|
||||
|
||||
export type TabProps = TabsPrimitive.TabsTriggerProps;
|
||||
|
||||
export const Tab = ({ className, children, ...props }: TabProps) => (
|
||||
export const Tab = ({
|
||||
className,
|
||||
children,
|
||||
variant = "project",
|
||||
...props
|
||||
}: TabProps & { variant?: "project" | "namespace" | "org" }) => (
|
||||
<TabsPrimitive.Trigger
|
||||
className={twMerge(
|
||||
"flex h-10 items-center justify-center px-3 text-sm font-medium text-mineshaft-400 transition-all select-none first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 data-[state=active]:border-b data-[state=active]:border-primary data-[state=active]:text-white",
|
||||
"flex h-10 items-center justify-center px-3 text-sm font-medium text-mineshaft-400 transition-all select-none first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 data-[state=active]:border-b data-[state=active]:text-white",
|
||||
variant === "project" && "data-[state=active]:border-primary",
|
||||
variant === "namespace" && "data-[state=active]:border-namespace-v1",
|
||||
variant === "org" && "data-[state=active]:border-org-v1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -337,7 +337,8 @@ export const useMoveSecrets = ({
|
||||
destinationSecretPath,
|
||||
secretIds,
|
||||
shouldOverwrite,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
projectId
|
||||
}) => {
|
||||
const { data } = await apiRequest.post<{
|
||||
isSourceUpdated: boolean;
|
||||
@@ -349,7 +350,8 @@ export const useMoveSecrets = ({
|
||||
destinationSecretPath,
|
||||
secretIds,
|
||||
shouldOverwrite,
|
||||
projectSlug
|
||||
projectSlug,
|
||||
projectId
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
/*legacy color schema */
|
||||
/* Fonts */
|
||||
--font-inter: "Inter", sans-serif;
|
||||
--color-org-v1: #30B3FF;
|
||||
--color-namespace-v1: #96ff59;
|
||||
|
||||
/* Primary */
|
||||
--color-primary-50: #fffff5;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const KmsLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
@@ -55,7 +55,7 @@ export const KmsLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faLock} />
|
||||
@@ -74,12 +74,12 @@ export const KmsLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Management
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -91,7 +91,7 @@ export const KmsLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
@@ -108,12 +108,12 @@ export const KmsLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -125,6 +125,7 @@ export const KmsLayout = () => {
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
|
||||
@@ -4,11 +4,11 @@ import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-ic
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBook,
|
||||
faBuilding,
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faEnvelope,
|
||||
faExclamationTriangle,
|
||||
faGlobe,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faServer,
|
||||
@@ -20,11 +20,13 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
Badge,
|
||||
BreadcrumbContainer,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -199,6 +201,8 @@ export const Navbar = () => {
|
||||
|
||||
const isServerAdminPanel = location.pathname.startsWith("/admin");
|
||||
|
||||
const isOrgScope = breadcrumbs?.length === 1; // TODO: scott/akhil is this adequate?
|
||||
|
||||
return (
|
||||
<div className="z-10 flex min-h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4">
|
||||
<div>
|
||||
@@ -230,10 +234,13 @@ export const Navbar = () => {
|
||||
<DropdownMenu modal={false}>
|
||||
<Link to="/organization/projects">
|
||||
<div className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faBuilding} className="text-xs text-bunker-300" />
|
||||
</div>
|
||||
<div className="whitespace-nowrap">{currentOrg?.name}</div>
|
||||
<Badge
|
||||
variant="org"
|
||||
className={twMerge("text-sm", !isOrgScope && "bg-transparent opacity-75")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGlobe} />
|
||||
{currentOrg?.name}
|
||||
</Badge>
|
||||
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
|
||||
{getPlan(subscription)}
|
||||
</div>
|
||||
|
||||
@@ -52,31 +52,31 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
<MenuGroup title="Overview">
|
||||
<Link to="/organization/projects">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faTable} />
|
||||
</div>
|
||||
Projects
|
||||
Overview
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/access-management">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Control
|
||||
Organization Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/billing">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
|
||||
@@ -88,7 +88,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
</Link>
|
||||
<Link to="/organization/audit-logs">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} className="mr-4" />
|
||||
@@ -100,7 +100,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} className="mr-4" />
|
||||
@@ -114,7 +114,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
<MenuGroup title="Resources">
|
||||
<Link to="/organization/app-connections">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} className="mr-4" />
|
||||
@@ -126,7 +126,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
</Link>
|
||||
<Link to="/organization/networking">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="org" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mr-4" />
|
||||
@@ -145,6 +145,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
!subscription.has_used_trial && (
|
||||
<Tooltip content="Start Free Pro Trial">
|
||||
<MenuItem
|
||||
variant="org"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<FontAwesomeIcon
|
||||
@@ -170,6 +171,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
)}
|
||||
<Link to="/organization/secret-sharing">
|
||||
<MenuItem
|
||||
variant="org"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
@@ -183,6 +185,7 @@ export const OrgSidebar = ({ isHidden }: Props) => {
|
||||
{user.superAdmin && (
|
||||
<Link to="/admin">
|
||||
<MenuItem
|
||||
variant="org"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
|
||||
@@ -59,7 +59,7 @@ export const PamLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
@@ -76,7 +76,7 @@ export const PamLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBoxOpen} />
|
||||
@@ -93,7 +93,7 @@ export const PamLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faDisplay} />
|
||||
@@ -112,12 +112,12 @@ export const PamLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Management
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -129,7 +129,7 @@ export const PamLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
@@ -146,12 +146,12 @@ export const PamLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faSitemap} />
|
||||
@@ -71,7 +71,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faFileLines} />
|
||||
@@ -88,7 +88,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCertificate} />
|
||||
@@ -105,7 +105,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faStamp} />
|
||||
@@ -122,7 +122,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBell} />
|
||||
@@ -139,7 +139,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} />
|
||||
@@ -156,7 +156,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} />
|
||||
@@ -175,12 +175,12 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Management
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -192,7 +192,7 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
@@ -209,12 +209,12 @@ export const PkiManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
@@ -3,10 +3,10 @@ import { faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faCube,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faStar as faSolidStar,
|
||||
faTable
|
||||
faStar as faSolidStar
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, linkOptions } from "@tanstack/react-router";
|
||||
@@ -16,6 +16,7 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { NewProjectModal } from "@app/components/projects";
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -106,14 +107,16 @@ export const ProjectSelect = () => {
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faTable} className="text-xs text-bunker-300" />
|
||||
</div>
|
||||
<div className="relative flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary">
|
||||
<Tooltip content={currentWorkspace.name} className="max-w-96 break-words">
|
||||
<div className="max-w-44 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<Badge
|
||||
variant="project"
|
||||
className="max-w-44 overflow-hidden text-sm text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCube} />
|
||||
|
||||
{currentWorkspace?.name}
|
||||
</div>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -82,6 +82,7 @@ export const SecretManagerLayout = () => {
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem
|
||||
variant="project"
|
||||
isSelected={
|
||||
isActive ||
|
||||
location.pathname.startsWith(
|
||||
@@ -105,7 +106,7 @@ export const SecretManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} />
|
||||
@@ -123,7 +124,7 @@ export const SecretManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faArrowsSpin} />
|
||||
@@ -141,7 +142,7 @@ export const SecretManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCheckToSlot} />
|
||||
@@ -166,7 +167,7 @@ export const SecretManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} />
|
||||
@@ -185,12 +186,12 @@ export const SecretManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Management
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -202,7 +203,7 @@ export const SecretManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
@@ -219,12 +220,12 @@ export const SecretManagerLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -236,6 +237,7 @@ export const SecretManagerLayout = () => {
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
|
||||
@@ -69,7 +69,7 @@ export const SecretScanningLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faDatabase} />
|
||||
@@ -86,7 +86,7 @@ export const SecretScanningLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex w-full gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
@@ -108,7 +108,7 @@ export const SecretScanningLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faPlug} />
|
||||
@@ -127,12 +127,12 @@ export const SecretScanningLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Management
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -144,7 +144,7 @@ export const SecretScanningLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
@@ -166,7 +166,7 @@ export const SecretScanningLayout = () => {
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -178,6 +178,7 @@ export const SecretScanningLayout = () => {
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
|
||||
@@ -51,7 +51,7 @@ export const SshLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faServer} />
|
||||
@@ -74,7 +74,7 @@ export const SshLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faStamp} />
|
||||
@@ -97,12 +97,12 @@ export const SshLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faUsers} />
|
||||
</div>
|
||||
Access Management
|
||||
Project Access
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -114,7 +114,7 @@ export const SshLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
@@ -131,12 +131,12 @@ export const SshLayout = () => {
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive}>
|
||||
<MenuItem variant="project" isSelected={isActive}>
|
||||
<div className="mx-1 flex gap-2">
|
||||
<div className="w-6">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
Project Settings
|
||||
</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -148,6 +148,7 @@ export const SshLayout = () => {
|
||||
<Menu>
|
||||
<Link to="/organization/projects">
|
||||
<MenuItem
|
||||
variant="project"
|
||||
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
|
||||
leftIcon={
|
||||
<div className="w-6">
|
||||
|
||||
@@ -16,6 +16,7 @@ export const AccessManagementPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Access Control"
|
||||
description="Manage server admins within your Infisical instance."
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const AuthenticationPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Authentication"
|
||||
description="Manage authentication settings for your Infisical instance."
|
||||
/>
|
||||
|
||||
@@ -15,7 +15,11 @@ export const CachingPage = () => {
|
||||
</Helmet>
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Caching" description="Manage caching for your Infisical instance." />
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Caching"
|
||||
description="Manage caching for your Infisical instance."
|
||||
/>
|
||||
<CachingPageForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const EncryptionPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Encryption"
|
||||
description="Manage encryption settings for your Infisical instance."
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const EnvironmentPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Environment Variables"
|
||||
description="Manage the environment variables for your Infisical instance."
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,7 @@ export const GeneralPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="General"
|
||||
description="Manage general settings for your Infisical instance."
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const IntegrationsPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Integrations"
|
||||
description="Manage integrations for your Infisical instance."
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const ResourceOverviewPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Resource Overview"
|
||||
description="Manage resources within your Infisical instance."
|
||||
/>
|
||||
|
||||
@@ -153,7 +153,7 @@ const UserPanelTable = ({
|
||||
icon={adminsOnly && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<FontAwesomeIcon icon={faUserShield} className="text-yellow-700" />
|
||||
<span>Server Admins</span>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const AlertingPage = () => {
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Alerting"
|
||||
description="Configure alerts for expiring certificates and CAs to maintain security and compliance."
|
||||
/>
|
||||
|
||||
@@ -87,7 +87,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.name}>
|
||||
<PageHeader scope="project" title={data.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
@@ -17,6 +17,7 @@ export const CertificateAuthoritiesPage = () => {
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Certificate Authorities"
|
||||
description="Manage certificate authorities for issuing and signing certificates"
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@ export const CertificatesPage = () => {
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Certificates"
|
||||
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
|
||||
/>
|
||||
|
||||
@@ -45,6 +45,7 @@ export const IntegrationsListPage = () => {
|
||||
<div className="relative container mx-auto max-w-7xl pb-12 text-white">
|
||||
<div className="mb-8">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Integrations"
|
||||
description="Manage integrations with third-party certificate services."
|
||||
/>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const PkiCollectionPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.name}>
|
||||
<PageHeader scope="project" title={data.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
@@ -78,7 +78,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.name}>
|
||||
<PageHeader scope="project" title={data.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
@@ -16,6 +16,7 @@ export const PkiSubscribersPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Subscribers"
|
||||
description="Manage subscribers that request and receive certificates, including user devices, servers, and services."
|
||||
/>
|
||||
|
||||
@@ -106,6 +106,7 @@ export const PkiTemplateListPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Certificate Templates"
|
||||
description="Manage certificate template to request and issue dynamic certificates following a strict format."
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const SettingsPage = () => {
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title={t("settings.project.title")} />
|
||||
<PageHeader scope="project" title={t("settings.project.title")} />
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -18,6 +18,7 @@ export const KmipPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="KMIP"
|
||||
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,8 @@ export const OverviewPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Overview Page"
|
||||
scope="project"
|
||||
title="Overview"
|
||||
description="Manage keys and perform cryptographic operations."
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
|
||||
@@ -21,7 +21,7 @@ export const SettingsPage = () => {
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Settings" />
|
||||
<PageHeader scope="project" title="Settings" />
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -82,7 +82,8 @@ export const AccessManagementPage = () => {
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Organization Access Control"
|
||||
scope="org"
|
||||
title="Access Control"
|
||||
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
|
||||
/>
|
||||
{!currentOrg.shouldUseNewPrivilegeSystem && (
|
||||
@@ -115,7 +116,7 @@ export const AccessManagementPage = () => {
|
||||
{tabSections
|
||||
.filter((el) => !el.isHidden)
|
||||
.map((el) => (
|
||||
<Tab value={el.key} key={`org-access-tab-${el.key}`}>
|
||||
<Tab variant="org" value={el.key} key={`org-access-tab-${el.key}`}>
|
||||
{el.label}
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
@@ -22,6 +22,7 @@ export const AppConnectionsPage = withPermission(
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
className="w-full"
|
||||
title="App Connections"
|
||||
description="Manage organization App Connections"
|
||||
|
||||
@@ -16,7 +16,8 @@ export const AuditLogsPage = () => {
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
title="Audit logs"
|
||||
scope="org"
|
||||
title="Audit Logs"
|
||||
description="Audit logs for security and compliance teams to monitor information access."
|
||||
/>
|
||||
<LogsSection pageView />
|
||||
|
||||
@@ -18,8 +18,9 @@ export const BillingPage = () => {
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl px-6">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
title={t("billing.title")}
|
||||
description="View your billing plan, next billing cycle."
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,9 @@ export const BillingTabGroup = withPermission(
|
||||
<Tabs defaultValue={tabs[0].key}>
|
||||
<TabList>
|
||||
{tabsFiltered.map((tab) => (
|
||||
<Tab value={tab.key}>{tab.name}</Tab>
|
||||
<Tab variant="org" value={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanel value={tabs[0].key}>
|
||||
|
||||
@@ -83,7 +83,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.group.name}>
|
||||
<PageHeader scope="org" title={data.group.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
@@ -75,7 +75,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.identity.name} />
|
||||
<PageHeader scope="org" title={data.identity.name} />
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||
|
||||
@@ -14,6 +14,7 @@ export const NetworkingPage = () => {
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
title="Networking"
|
||||
description="Manage gateways and relays to securely access private network resources from Infisical"
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const NetworkingTabGroup = () => {
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
@@ -65,7 +65,8 @@ export const ProjectsPage = () => {
|
||||
</Helmet>
|
||||
<div className="mb-4 flex flex-col items-start justify-start">
|
||||
<PageHeader
|
||||
title="Projects"
|
||||
scope="org"
|
||||
title="Overview"
|
||||
description="Your team's complete security toolkit - organized and ready when you need them."
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -81,6 +81,7 @@ export const Page = () => {
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
title={
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
|
||||
@@ -22,6 +22,7 @@ export const SecretSharingPage = () => {
|
||||
<div className="h-full">
|
||||
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
title="Secret Sharing"
|
||||
description="Share secrets securely using a shareable link"
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const SecretSharingSettingsPage = withPermission(
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Secret Share Settings" />
|
||||
<PageHeader scope="org" title="Secret Share Settings" />
|
||||
<SecretSharingSettingsTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export const SettingsPage = () => {
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title={t("settings.org.title")} />
|
||||
<PageHeader scope="org" title={t("settings.org.title")} />
|
||||
<OrgTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@ export const OrgTabGroup = () => {
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
@@ -119,6 +119,7 @@ const Page = withPermission(
|
||||
{membership && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
title={
|
||||
membership.user.firstName || membership.user.lastName
|
||||
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
|
||||
|
||||
@@ -23,7 +23,11 @@ export const PamAccountsPage = () => {
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Accounts" description="View, access, and manage accounts." />
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Accounts"
|
||||
description="View, access, and manage accounts."
|
||||
/>
|
||||
<PamAccountsSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ export const PamResourcesPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Resources"
|
||||
description="Manage resources such as servers, databases, and more."
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,7 @@ const Page = () => {
|
||||
{session && (
|
||||
<div className="mx-auto mb-6 flex w-full max-w-7xl flex-col">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title={`${session.accountName} Session`}
|
||||
description={`View details for this ${session.accountName} session.`}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ export const PamSessionPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Sessions"
|
||||
description="Filter and search through account sessions."
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ export const SettingsPage = () => {
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Settings" description="Configure your PAM project." />
|
||||
<PageHeader scope="project" title="Settings" description="Configure your PAM project." />
|
||||
<Tabs defaultValue="tab-project-general">
|
||||
<TabList>
|
||||
<Tab value="tab-project-general">General</Tab>
|
||||
|
||||
@@ -40,6 +40,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Access Control"
|
||||
description="Manage fine-grained access for users, groups, roles, and identities within your project resources."
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,7 @@ export const AppConnectionsPage = withProjectPermission(
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
className="w-full"
|
||||
title="App Connections"
|
||||
description="Manage project App Connections"
|
||||
|
||||
@@ -16,6 +16,7 @@ export const AuditLogsPage = () => {
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Audit logs"
|
||||
description="Audit logs for security and compliance teams to monitor information access."
|
||||
/>
|
||||
|
||||
@@ -34,7 +34,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{groupMembership ? (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={groupMembership.group.name} />
|
||||
<PageHeader scope="project" title={groupMembership.group.name} />
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<GroupDetailsSection groupMembership={groupMembership} />
|
||||
|
||||
@@ -117,6 +117,7 @@ const Page = () => {
|
||||
{identityMembershipDetails ? (
|
||||
<>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title={identityMembershipDetails?.identity?.name}
|
||||
description={`Identity joined on ${identityMembershipDetails?.createdAt && formatRelative(new Date(identityMembershipDetails?.createdAt || ""), new Date())}`}
|
||||
>
|
||||
|
||||
@@ -119,6 +119,7 @@ export const Page = () => {
|
||||
{membershipDetails ? (
|
||||
<>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title={
|
||||
membershipDetails.user.firstName || membershipDetails.user.lastName
|
||||
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
|
||||
|
||||
@@ -90,6 +90,7 @@ const Page = () => {
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title={
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
|
||||
@@ -260,6 +260,7 @@ export const CommitDetailsTab = ({
|
||||
Commit History
|
||||
</Button>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title={`${parsedCommitDetails.changes?.message}` || "No message"}
|
||||
description={
|
||||
<>
|
||||
|
||||
@@ -307,6 +307,7 @@ export const RollbackPreviewTab = (): JSX.Element => {
|
||||
<div className="h-full w-full">
|
||||
<div>
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title={`Restore folder at commit ${selectedCommitId.substring(0, 8)}`}
|
||||
description={`Will return all changes in this folder to how they appeared at the point of commit ${selectedCommitId.substring(0, 8)}. Any modifications made after this commit will be undone.`}
|
||||
/>
|
||||
|
||||
@@ -49,6 +49,7 @@ export const CommitsPage = () => {
|
||||
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
|
||||
<div className="w-full max-w-[75vw]">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Commits"
|
||||
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
||||
/>
|
||||
|
||||
@@ -94,6 +94,7 @@ export const IntegrationDetailsByIDPage = () => {
|
||||
{integration ? (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title={`${integrationSlugNameMapping[integration.integration]} Integration`}
|
||||
>
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -48,6 +48,7 @@ export const IntegrationsListPage = () => {
|
||||
<div className="relative container mx-auto max-w-7xl pb-12 text-white">
|
||||
<div className="mb-8">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Integrations"
|
||||
description="Manage integrations with third-party services."
|
||||
/>
|
||||
|
||||
@@ -914,6 +914,7 @@ export const OverviewPage = () => {
|
||||
<div className="relative mx-auto max-w-7xl text-mineshaft-50 dark:scheme-dark">
|
||||
<div className="flex w-full items-baseline justify-between">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Secrets Overview"
|
||||
description={
|
||||
<p className="text-md text-bunker-300">
|
||||
|
||||
@@ -82,6 +82,7 @@ type Props = {
|
||||
isImported: boolean;
|
||||
}[];
|
||||
}[];
|
||||
isSecretPresent?: boolean;
|
||||
};
|
||||
|
||||
export const SecretEditRow = ({
|
||||
@@ -101,7 +102,8 @@ export const SecretEditRow = ({
|
||||
isRotatedSecret,
|
||||
importedBy,
|
||||
importedSecret,
|
||||
isEmpty
|
||||
isEmpty,
|
||||
isSecretPresent
|
||||
}: Props) => {
|
||||
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
|
||||
"editSecret"
|
||||
@@ -113,20 +115,21 @@ export const SecretEditRow = ({
|
||||
|
||||
const [isFieldFocused, setIsFieldFocused] = useToggle();
|
||||
|
||||
const fetchSecretValueParams = importedSecret
|
||||
? {
|
||||
environment: importedSecret.environment,
|
||||
secretPath: importedSecret.secretPath,
|
||||
secretKey: importedSecret.secret?.key ?? "",
|
||||
projectId: currentProject.id
|
||||
}
|
||||
: {
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: secretName,
|
||||
projectId: currentProject.id,
|
||||
isOverride
|
||||
};
|
||||
const fetchSecretValueParams =
|
||||
importedSecret && !isSecretPresent
|
||||
? {
|
||||
environment: importedSecret.environment,
|
||||
secretPath: importedSecret.secretPath,
|
||||
secretKey: importedSecret.secret?.key ?? "",
|
||||
projectId: currentProject.id
|
||||
}
|
||||
: {
|
||||
environment,
|
||||
secretPath,
|
||||
secretKey: secretName,
|
||||
projectId: currentProject.id,
|
||||
isOverride
|
||||
};
|
||||
|
||||
// scott: only fetch value if secret exists, has non-empty value and user has permission
|
||||
const canFetchValue = Boolean(importedSecret ?? secretId) && !isEmpty && !secretValueHidden;
|
||||
|
||||
@@ -284,6 +284,7 @@ export const SecretOverviewTableRow = ({
|
||||
environment={slug}
|
||||
isRotatedSecret={secret?.isRotatedSecret}
|
||||
importedBy={importedBy}
|
||||
isSecretPresent={Boolean(secret)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -40,6 +40,7 @@ export const SecretApprovalsPage = () => {
|
||||
</Helmet>
|
||||
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 text-white">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Approval Workflows"
|
||||
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
||||
/>
|
||||
|
||||
@@ -762,6 +762,7 @@ const Page = () => {
|
||||
return (
|
||||
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:scheme-dark">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Secrets Management"
|
||||
description={
|
||||
<p className="text-md text-bunker-300">
|
||||
|
||||
@@ -79,7 +79,7 @@ type Props = {
|
||||
isSelected?: boolean;
|
||||
onToggleSecretSelect: (secret: SecretV3RawSanitized) => void;
|
||||
tags: WsTag[];
|
||||
onCreateTag: () => void;
|
||||
onCreateTag: (secret?: SecretV3RawSanitized) => void;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
onShareSecret: (sec: SecretV3RawSanitized) => void;
|
||||
@@ -731,7 +731,7 @@ export const SecretItem = memo(
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
onClick={() => onCreateTag(secret)}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
@@ -924,7 +924,7 @@ export const SecretItem = memo(
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateTag}
|
||||
onClick={() => onCreateTag(secret)}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
|
||||
@@ -465,6 +465,22 @@ export const SecretListView = ({
|
||||
[environment, secretPath, isProtectedBranch, isBatchMode, projectId, addPendingChange]
|
||||
);
|
||||
|
||||
// Function to append newly created tag to the current secret
|
||||
const append = useCallback(
|
||||
(newTag: WsTag) => {
|
||||
const currentSecret = popUp.createTag.data as SecretV3RawSanitized;
|
||||
if (!currentSecret) return;
|
||||
|
||||
const updatedTags = [...(currentSecret.tags || []), { id: newTag.id, slug: newTag.slug }];
|
||||
|
||||
handleSaveSecret(currentSecret, {
|
||||
...currentSecret,
|
||||
tags: updatedTags
|
||||
});
|
||||
},
|
||||
[popUp.createTag.data, handleSaveSecret]
|
||||
);
|
||||
|
||||
const handleSecretDelete = useCallback(async () => {
|
||||
const {
|
||||
key,
|
||||
@@ -552,7 +568,13 @@ export const SecretListView = ({
|
||||
]);
|
||||
|
||||
// for optimization on minimise re-rendering of secret items
|
||||
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
|
||||
const onCreateTag = useCallback((secret?: SecretV3RawSanitized) => {
|
||||
if (secret) {
|
||||
handlePopUpOpen("createTag", secret);
|
||||
} else {
|
||||
handlePopUpOpen("createTag");
|
||||
}
|
||||
}, []);
|
||||
const onDeleteSecret = useCallback(
|
||||
(sec: SecretV3RawSanitized) => handlePopUpOpen("deleteSecret", sec),
|
||||
[]
|
||||
@@ -640,6 +662,8 @@ export const SecretListView = ({
|
||||
<CreateTagModal
|
||||
isOpen={popUp.createTag.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
|
||||
append={append}
|
||||
currentSecret={popUp.createTag.data}
|
||||
/>
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</>
|
||||
|
||||
@@ -147,6 +147,7 @@ const Page = () => {
|
||||
return (
|
||||
<div className="container mx-auto w-full max-w-7xl bg-bunker-800 text-white">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Secret Rotation"
|
||||
description="Stop manually rotating secrets and automate credential rotation."
|
||||
>
|
||||
|
||||
@@ -42,6 +42,7 @@ export const SettingsPage = () => {
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Settings"
|
||||
description="Configure your secret manager's encryption, environments, webhooks and other configurations."
|
||||
/>
|
||||
|
||||
@@ -26,6 +26,7 @@ export const SecretScanningDataSourcesPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Data Sources"
|
||||
description="Manage your Secret Scanning data sources."
|
||||
/>
|
||||
|
||||
@@ -23,7 +23,11 @@ export const SecretScanningFindingsPage = () => {
|
||||
<div className="h-full bg-bunker-800">
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title="Findings" description="View Secret Leaks across your project." />
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Findings"
|
||||
description="View Secret Leaks across your project."
|
||||
/>
|
||||
<SecretScanningFindingsSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ export const SettingsPage = () => {
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Settings"
|
||||
description="Configure your Secret Scanning product's configurations."
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,11 @@ export const SettingsPage = () => {
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
</Helmet>
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader title="Settings" description="Configure your SSH product's configurations." />
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="Settings"
|
||||
description="Configure your SSH product's configurations."
|
||||
/>
|
||||
<Tabs defaultValue="tab-project-general">
|
||||
<TabList>
|
||||
<Tab value="tab-project-general">General</Tab>
|
||||
|
||||
@@ -70,7 +70,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.friendlyName}>
|
||||
<PageHeader scope="project" title={data.friendlyName}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
@@ -16,6 +16,7 @@ export const SshCasPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="SSH Certificate Authorities"
|
||||
description="Manage the SSH certificate authorities used to sign user and host certificates, including custom and default CAs."
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const SshCertsPage = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader
|
||||
scope="project"
|
||||
title="SSH Certificates"
|
||||
description="View and audit all issued SSH certificates, including validity and associated access metadata."
|
||||
/>
|
||||
|
||||
@@ -71,7 +71,7 @@ const Page = () => {
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||
<PageHeader title={data.name}>
|
||||
<PageHeader scope="project" title={data.name}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user