Compare commits

...

26 Commits

Author SHA1 Message Date
4c8063c532 docs: update .net sdk 2025-06-18 01:51:33 +04:00
f4ae40cb86 Merge pull request #3805 from Infisical/access-control-tab-consistency
improvement(project-access-control): minor UI adjustments for consistency
2025-06-17 12:47:11 -07:00
14449b8b41 improvements: address feedback 2025-06-17 12:17:53 -07:00
bcdcaa33a4 Merge pull request #3807 from Infisical/conditional-dynamic-secret-access-display
improvement(access-tree): dynamic secret conditional display
2025-06-17 11:49:45 -07:00
e8a8542757 Merge pull request #3803 from Infisical/project-roles-table-improvements
improvement(project-roles): Add pagination, search and column sorting to Project Roles table
2025-06-17 11:49:31 -07:00
e61d35d824 Merge pull request #3808 from Infisical/daniel/fix-editing-value-hidden-secrets
fix: editing secrets with value hidden
2025-06-17 22:08:50 +04:00
714d6831bd Update SecretListView.tsx 2025-06-17 21:23:30 +04:00
956f75eb43 fix: editing secrets with value hidden 2025-06-17 21:02:47 +04:00
bfee34f38d Merge pull request #3801 from Infisical/doc/production-hardening
doc: production hardening
2025-06-17 22:10:22 +08:00
092b89c59e misc: corrected kms section 2025-06-17 20:28:28 +08:00
3d76ae3399 misc: some more updates in examples 2025-06-17 20:25:38 +08:00
23aa97feff misc: addressed comments 2025-06-17 20:17:17 +08:00
0c5155f8e6 improvement: minor UI adjustments to make project access control tabs more uniform 2025-06-16 17:17:55 -07:00
4afe2f2377 improvements: use stored preferred page size for project roles table and add reset helper 2025-06-16 16:36:03 -07:00
1e07c2fe23 improvements: add sorting, search, and pagination to project roles table and improve dropdown menu 2025-06-16 15:00:40 -07:00
191486519f Merge branch 'doc/production-hardening' of https://github.com/Infisical/infisical into doc/production-hardening 2025-06-17 03:45:54 +08:00
cab8fb0d8e misc: reorganized 2025-06-17 03:45:35 +08:00
8bfd728ce4 misc: added mint json 2025-06-16 19:22:35 +00:00
c9eab0af18 misc: updated section on db 2025-06-17 03:21:26 +08:00
d7dfc531fc doc: added guide for production hardening 2025-06-17 03:20:11 +08:00
a89bd08c08 Merge pull request #3795 from Infisical/ENG-2928
feat(machine-identities): Delete protection
2025-06-16 14:57:45 -04:00
4bfb9e8e74 Merge pull request #3789 from Infisical/misc/add-custom-role-slug-in-fetch-group
misc: add custom role slug in fetch group
2025-06-16 22:40:37 +04:00
c12bfa766c Review fixes 2025-06-13 14:51:39 -04:00
3432a16d4f Update frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-13 14:45:38 -04:00
19a403f467 feat(machine-identities): Delete protection 2025-06-13 14:37:15 -04:00
06a7e804eb misc: add custom role slug in fetch group 2025-06-13 17:26:36 +08:00
35 changed files with 1256 additions and 205 deletions

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
if (!hasCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.boolean("hasDeleteProtection").notNullable().defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
if (hasCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.dropColumn("hasDeleteProtection");
});
}
}

View File

@ -12,7 +12,8 @@ export const IdentitiesSchema = z.object({
name: z.string(),
authMethod: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
hasDeleteProtection: z.boolean().default(false)
});
export type TIdentities = z.infer<typeof IdentitiesSchema>;

View File

@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
}),
response: {
200: GroupsSchema
200: GroupsSchema.extend({
customRoleSlug: z.string().nullable()
})
}
},
handler: async (req) => {

View File

@ -780,6 +780,7 @@ interface CreateIdentityEvent {
metadata: {
identityId: string;
name: string;
hasDeleteProtection: boolean;
};
}
@ -788,6 +789,7 @@ interface UpdateIdentityEvent {
metadata: {
identityId: string;
name?: string;
hasDeleteProtection?: boolean;
};
}

View File

@ -169,11 +169,29 @@ export const groupDALFactory = (db: TDbClient) => {
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.Groups)
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.where(`${TableName.Groups}.id`, id)
.select(
selectAllTableCols(TableName.Groups),
db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles)
)
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
};
return {
...groupOrm,
findGroups,
findByOrgId,
findAllGroupPossibleMembers,
findGroupsByProjectId,
...groupOrm
findById
};
};

View File

@ -111,12 +111,14 @@ export const IDENTITIES = {
CREATE: {
name: "The name of the identity to create.",
organizationId: "The organization ID to which the identity belongs.",
role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'."
role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
},
UPDATE: {
identityId: "The ID of the identity to update.",
name: "The new name of the identity.",
role: "The new role of the identity."
role: "The new role of the identity.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
},
DELETE: {
identityId: "The ID of the identity to delete."

View File

@ -44,6 +44,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
name: z.string().trim().describe(IDENTITIES.CREATE.name),
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array()
@ -75,6 +76,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
type: EventType.CREATE_IDENTITY,
metadata: {
name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id
}
}
@ -86,6 +88,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
properties: {
orgId: req.body.organizationId,
name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id,
...req.auditLogInfo
}
@ -117,6 +120,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
body: z.object({
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array()
@ -148,6 +152,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
type: EventType.UPDATE_IDENTITY,
metadata: {
name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id
}
}
@ -243,7 +248,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true,
description: true
}).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string())
})
})
@ -292,7 +297,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true,
description: true
}).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string())
})
}).array(),
@ -386,7 +391,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true,
description: true
}).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string())
})
}).array(),
@ -451,7 +456,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
temporaryAccessEndTime: z.date().nullable().optional()
})
),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true, type: true })

View File

@ -101,6 +101,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
@ -130,6 +131,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
data: docs,
parentMapper: ({
identityName,
hasDeleteProtection,
uaId,
awsId,
gcpId,
@ -151,6 +153,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identity: {
id: identityId,
name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({
uaId,
awsId,

View File

@ -114,16 +114,18 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
db.ref("name").withSchema(TableName.Identity)
db.ref("name").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
);
if (data) {
const { name } = data;
const { name, hasDeleteProtection } = data;
return {
...data,
identity: {
id: data.identityId,
name,
hasDeleteProtection,
authMethods: buildAuthMethods(data)
}
};
@ -155,7 +157,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("name").withSchema(TableName.Identity).as("identityName")
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
)
.where(filter)
.as("paginatedIdentity");
@ -245,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema("paginatedIdentity"),
db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
db.ref("identityName").withSchema("paginatedIdentity"),
db.ref("hasDeleteProtection").withSchema("paginatedIdentity"),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
@ -286,6 +290,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName,
identityId,
identityName,
hasDeleteProtection,
role,
roleId,
id,
@ -324,6 +329,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: {
id: identityId,
name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({
uaId,
alicloudId,
@ -476,6 +482,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
@ -518,6 +525,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName,
identityId,
identityName,
hasDeleteProtection,
role,
roleId,
total_count,
@ -556,6 +564,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: {
id: identityId,
name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({
uaId,
alicloudId,

View File

@ -47,6 +47,7 @@ export const identityServiceFactory = ({
const createIdentity = async ({
name,
role,
hasDeleteProtection,
actor,
orgId,
actorId,
@ -96,7 +97,7 @@ export const identityServiceFactory = ({
}
const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = await identityDAL.create({ name }, tx);
const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx);
await identityOrgMembershipDAL.create(
{
identityId: newIdentity.id,
@ -138,6 +139,7 @@ export const identityServiceFactory = ({
const updateIdentity = async ({
id,
role,
hasDeleteProtection,
name,
actor,
actorId,
@ -189,7 +191,11 @@ export const identityServiceFactory = ({
}
const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
const newIdentity =
name || hasDeleteProtection
? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx)
: await identityDAL.findById(id, tx);
if (role) {
await identityOrgMembershipDAL.updateById(
identityOrgMembership.id,
@ -272,6 +278,9 @@ export const identityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
if (identityOrgMembership.identity.hasDeleteProtection)
throw new BadRequestError({ message: "Identity has delete protection" });
const deletedIdentity = await identityDAL.deleteById(id);
await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId);

View File

@ -5,12 +5,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
export type TCreateIdentityDTO = {
role: string;
name: string;
hasDeleteProtection: boolean;
metadata?: { key: string; value: string }[];
} & TOrgPermission;
export type TUpdateIdentityDTO = {
id: string;
role?: string;
hasDeleteProtection?: boolean;
name?: string;
metadata?: { key: string; value: string }[];
isActorSuperAdmin?: boolean;

View File

@ -81,6 +81,7 @@ export type TMachineIdentityCreatedEvent = {
event: PostHogEventTypes.MachineIdentityCreated;
properties: {
name: string;
hasDeleteProtection: boolean;
orgId: string;
identityId: string;
};

View File

@ -396,7 +396,8 @@
"pages": [
"self-hosting/guides/mongo-to-postgres",
"self-hosting/guides/custom-certificates",
"self-hosting/guides/automated-bootstrapping"
"self-hosting/guides/automated-bootstrapping",
"self-hosting/guides/production-hardening"
]
},
{
@ -664,9 +665,9 @@
"sdks/languages/node",
"sdks/languages/python",
"sdks/languages/java",
"sdks/languages/csharp",
"sdks/languages/go",
"sdks/languages/ruby",
"sdks/languages/csharp"
"sdks/languages/ruby"
]
},
{

View File

@ -1,9 +1,10 @@
---
title: "Infisical .NET SDK"
sidebarTitle: ".NET"
url: "https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk"
icon: "bars"
---
{/*
If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application.
- [Nuget Package](https://www.nuget.org/packages/Infisical.Sdk)
@ -590,4 +591,4 @@ var decryptedPlaintext = infisical.DecryptSymmetric(decryptOptions);
#### Returns (string)
`Plaintext` (string): The decrypted plaintext.
*/}

View File

@ -0,0 +1,697 @@
---
title: "Production Hardening"
description: "Security hardening recommendations for production Infisical deployments"
---
This document provides specific security hardening recommendations for production Infisical deployments. These recommendations follow Infisical's security model and focus on defense in depth.
Choose your deployment method below and follow the recommendations for your specific setup. Start with **Universal Security Fundamentals** that apply to all deployments, then follow your deployment-specific section.
## Universal Security Fundamentals
These security configurations apply to **all** Infisical deployments regardless of how you deploy.
### Cryptographic Security
#### Generate Secure Keys
Generate strong cryptographic keys for your deployment:
```bash
# Required - Generate secure encryption key
ENCRYPTION_KEY=$(openssl rand -hex 16)
# Required - Generate secure auth secret
AUTH_SECRET=$(openssl rand -base64 32)
```
#### Configure Token Lifetimes
Minimize exposure window for compromised tokens:
```bash
# JWT token configuration (adjust based on security requirements)
JWT_AUTH_LIFETIME=15m # Authentication tokens
JWT_REFRESH_LIFETIME=24h # Refresh tokens
JWT_SERVICE_LIFETIME=1h # Service tokens
```
### Network Security
#### TLS Configuration
Configure HTTPS and secure database connections:
```bash
# Enable HTTPS (recommended for production)
HTTPS_ENABLED=true
# Secure PostgreSQL connection with SSL
DB_CONNECTION_URI="postgresql://user:pass@host:5432/db?sslmode=require"
# For base64-encoded SSL certificate
DB_ROOT_CERT="<base64-encoded-certificate>"
```
#### Redis Security
Use authentication and TLS for Redis:
```bash
# Redis with TLS (if supported by your Redis deployment)
REDIS_URL="rediss://user:password@redis:6380"
# Redis Sentinel configuration for high availability
REDIS_SENTINEL_HOSTS="192.168.65.254:26379,192.168.65.254:26380"
REDIS_SENTINEL_MASTER_NAME="mymaster"
REDIS_SENTINEL_ENABLE_TLS=true
REDIS_SENTINEL_USERNAME="sentinel_user"
REDIS_SENTINEL_PASSWORD="sentinel_password"
```
#### Network Access Controls
Configure network restrictions and firewall rules:
```bash
# Limit CORS to specific domains
CORS_ALLOWED_ORIGINS=["https://your-app.example.com"]
# Prevent connections to internal/private IP addresses
# This blocks access to internal services like metadata endpoints,
# internal APIs, databases, and other sensitive infrastructure
ALLOW_INTERNAL_IP_CONNECTIONS=false
```
**Implement network firewalls**. Restrict network access to only necessary services:
- **Required ports**: Infisical API (8080) and HTTPS (if applicable)
- **Database access**: Restrict PostgreSQL and Redis to authorized sources only
- **Principle**: Default deny incoming, allow only required traffic
- **Implementation**: See your deployment-specific section below for exact configuration
### Application Security
#### Site Configuration
Set proper site URL for your Infisical instance:
```bash
# Required - Must be absolute URL with protocol
SITE_URL="https://app.infisical.com"
```
#### SMTP Security
Use TLS for email communications:
```bash
# SMTP with TLS
SMTP_HOST="smtp.example.com"
SMTP_PORT="587"
SMTP_USERNAME="your-smtp-user"
SMTP_PASSWORD="your-smtp-password"
SMTP_REQUIRE_TLS=true
SMTP_IGNORE_TLS=false
SMTP_FROM_ADDRESS="noreply@example.com"
SMTP_FROM_NAME="Infisical"
```
#### Privacy Configuration
Control telemetry and data collection:
```bash
# Optional - Disable telemetry (enabled by default)
TELEMETRY_ENABLED=false
```
### Database Security
#### High Availability Configuration
Configure database read replicas for high availability PostgreSQL setups:
```bash
# Read replica configuration (JSON format)
DB_READ_REPLICAS='[{"DB_CONNECTION_URI":"postgresql://user:pass@replica:5432/db?sslmode=require"}]'
```
### Operational Security
#### User Access Management
**Establish user off-boarding procedures**. Remove access promptly when users leave:
1. Remove user from organization
2. Revoke active service tokens
3. Remove from external identity providers
4. Audit access logs for the user's activity
5. Rotate any shared secrets the user had access to
#### Maintenance and Updates
**Keep frequent upgrade cadence**. Regularly update to the latest Infisical version for your deployment method.
## Deployment-Specific Hardening
### Docker Deployment
These recommendations are specific to Docker deployments of Infisical.
#### Container Security
**Use read-only root filesystems**. Prevent runtime modifications while allowing necessary temporary access:
```bash
# Run with read-only filesystem but allow /tmp access
docker run --read-only \
--tmpfs /tmp:rw,exec,size=1G \
infisical/infisical:latest
```
**Note**: Infisical requires temporary directory access for:
- Secret scanning operations
- SSH certificate generation and validation
The `--tmpfs` mounts provide secure, isolated temporary storage that is:
- Automatically cleaned up on container restart
- Limited in size to prevent disk exhaustion
- Isolated from the host system
- Wiped on container removal
**Drop unnecessary capabilities**. Remove all Linux capabilities:
```bash
# Drop all capabilities
docker run --cap-drop=ALL infisical/infisical:latest
```
**Use specific image tags**. Never use `latest` tags in production:
```bash
# Use specific version tags
docker run infisical/infisical:v0.93.1-postgres
```
#### Resource Management
**Set resource limits**. Prevent resource exhaustion attacks:
```bash
# Set memory and CPU limits
docker run --memory=1g --cpus=0.5 infisical/infisical:latest
```
#### Health Monitoring
**Configure health checks**. Set up Docker health checks:
```dockerfile
# In Dockerfile or docker-compose.yml
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/api/status || exit 1
```
#### Network Security
**Host firewall configuration**. Configure host-level firewall for Docker deployments:
```bash
# Docker manages its own iptables rules, but configure host firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow Docker-mapped ports (adjust based on your port mapping)
sudo ufw allow 8080/tcp # If mapping container 8080 to host 8080
sudo ufw allow 443/tcp # If terminating HTTPS at host level
# Enable firewall
sudo ufw --force enable
# Verify Docker iptables integration
sudo iptables -L DOCKER
```
#### Maintenance
**Regular updates**. Monitor [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) for new releases and update your image tags regularly.
### Kubernetes Deployment
These recommendations are specific to Kubernetes deployments of Infisical.
#### Pod Security
**Use Pod Security Standards**. Apply restricted security profile:
```yaml
# Namespace-level Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
name: infisical
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
```
**Configure security context**. Set comprehensive security context:
```yaml
# Deployment security context
apiVersion: apps/v1
kind: Deployment
metadata:
name: infisical
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
containers:
- name: infisical
image: infisical/infisical:v0.93.1-postgres
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1001
capabilities:
drop:
- ALL
resources:
limits:
memory: 1000Mi
cpu: 500m
requests:
cpu: 350m
memory: 512Mi
```
#### Network Security
**Configure network policies**. Restrict pod-to-pod communication:
```yaml
# Example Kubernetes NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: infisical-netpol
namespace: infisical
spec:
podSelector:
matchLabels:
app: infisical
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-system
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
- to:
- podSelector:
matchLabels:
app: redis
ports:
- protocol: TCP
port: 6379
```
**Infrastructure firewall considerations**. In addition to the universal host firewalls, implement infrastructure-level security:
For cloud deployments (AWS Security Groups, Azure NSGs, or GCP Firewall Rules):
- Allow ingress from load balancer to NodePort/ClusterIP service
- Allow egress to managed databases
- Block all other traffic
For on-premises deployments, ensure node-level firewalls allow:
- Ingress traffic from ingress controllers
- Egress traffic to external services (databases, SMTP)
#### Access Control
**Use dedicated service accounts**. Create service accounts with minimal permissions:
```yaml
# Service account configuration
apiVersion: v1
kind: ServiceAccount
metadata:
name: infisical
namespace: infisical
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: infisical
spec:
template:
spec:
serviceAccountName: infisical
```
#### Ingress Security
**Configure ingress with TLS**. Set up secure ingress:
```yaml
# Secure ingress configuration
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: infisical-ingress
namespace: infisical
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- secretName: infisical-tls
hosts:
- app.example.com
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: infisical
port:
number: 8080
```
#### Secret Management
**Use Kubernetes secrets**. Store sensitive configuration securely:
```yaml
# Kubernetes secret for environment variables
apiVersion: v1
kind: Secret
metadata:
name: infisical-secrets
namespace: infisical
type: Opaque
stringData:
AUTH_SECRET: "<generate-with-openssl-rand-base64-32>"
ENCRYPTION_KEY: "<generate-with-openssl-rand-hex-16>"
DB_CONNECTION_URI: "<your-postgres-connection-string>"
REDIS_URL: "<your-redis-connection-string>"
SITE_URL: "<your-site-url>"
```
**Note:** Kubernetes secrets are only base64-encoded by default and are not encrypted at rest unless you explicitly enable etcd encryption. For production environments, you should:
- Enable [etcd encryption at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) to protect secrets stored in the cluster
- Limit access to etcd and Kubernetes API to only trusted administrators
#### Health Monitoring
**Set up health checks**. Configure readiness and liveness probes:
```yaml
# Health check configuration
containers:
- name: infisical
readinessProbe:
httpGet:
path: /api/status
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/status
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
```
#### Infrastructure Considerations
**Use managed databases (if possible)**. For production deployments, consider using managed PostgreSQL and Redis services instead of in-cluster instances when feasible, as they typically provide better security, backup, and maintenance capabilities.
#### Maintenance
**Regular updates**. Monitor [Docker Hub](https://hub.docker.com/r/infisical/infisical/tags) for new releases and update your deployment manifests with new image tags regularly.
### Linux Binary Deployment
These recommendations are specific to Linux binary deployments of Infisical.
#### System User Management
**Create dedicated user account**. Run Infisical under a dedicated service account:
```bash
# Create dedicated user
sudo useradd --system --shell /bin/false --home-dir /opt/infisical infisical
# Create application directory
sudo mkdir -p /opt/infisical
sudo chown infisical:infisical /opt/infisical
```
#### Service Configuration
**Configure systemd service**. Create a secure systemd service:
```ini
# /etc/systemd/system/infisical.service
[Unit]
Description=Infisical Secret Management
After=network.target
[Service]
Type=simple
# IMPORTANT: Change from default 'root' user to dedicated service account
User=infisical
Group=infisical
WorkingDirectory=/opt/infisical
ExecStart=/opt/infisical/infisical-linux-amd64
Restart=always
RestartSec=10
# Security settings
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/infisical
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LimitCORE=0
MemorySwapMax=0
# Environment file
EnvironmentFile=/etc/infisical/environment
[Install]
WantedBy=multi-user.target
```
#### Configuration Security
**Secure environment configuration**. Store environment variables securely:
```bash
# Create secure config directory
sudo mkdir -p /etc/infisical
sudo chmod 750 /etc/infisical
sudo chown root:infisical /etc/infisical
# Create environment file
sudo touch /etc/infisical/environment
sudo chmod 640 /etc/infisical/environment
sudo chown root:infisical /etc/infisical/environment
```
#### System Security
**Disable memory swapping**. Prevent sensitive data from being written to disk:
```bash
# Disable swap immediately
sudo swapoff -a
# Disable swap permanently (comment out swap entries)
sudo sed -i '/swap/d' /etc/fstab
```
**Disable core dumps**. Prevent potential exposure of encryption keys:
```bash
# Set system-wide core dump limits
echo "* hard core 0" | sudo tee -a /etc/security/limits.conf
# Disable core dumps for current session
ulimit -c 0
```
#### File Permissions
**Secure file permissions**. Set proper permissions on application files:
```bash
# Set binary permissions
sudo chmod 755 /opt/infisical/infisical-linux-amd64
sudo chown infisical:infisical /opt/infisical/infisical-linux-amd64
# Set config file permissions
sudo chmod 640 /etc/infisical/environment
sudo chown root:infisical /etc/infisical/environment
```
#### Network Security
**Host firewall configuration**. Configure comprehensive firewall for Linux binary deployments:
```bash
# Configure UFW firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow Infisical API access
sudo ufw allow 8080/tcp
# Allow HTTPS (if terminating TLS at Infisical)
sudo ufw allow 443/tcp
# If running PostgreSQL locally, restrict to localhost
sudo ufw allow from 127.0.0.1 to any port 5432
# If running Redis locally, restrict to localhost
sudo ufw allow from 127.0.0.1 to any port 6379
# Enable firewall
sudo ufw --force enable
```
#### System Maintenance
**Synchronize system clocks**. Ensure accurate time for JWT tokens and audit logs:
```bash
# Install and configure NTP
sudo apt-get update
sudo apt-get install -y ntp
sudo systemctl enable ntp
sudo systemctl start ntp
# Verify time synchronization
timedatectl status
```
**Regular updates**. Monitor [Cloudsmith releases](https://cloudsmith.io/~infisical/repos/infisical-core/packages) for new binary versions and update your installation regularly.
## Enterprise Security Features
### Hardware Security Module (HSM) Integration
For the highest level of encryption security, integrate with Hardware Security Modules:
HSM integration provides hardware-protected encryption keys stored on tamper-proof devices, offering superior security for encryption operations:
- **Supported HSM Providers**: Thales Luna Cloud HSM, AWS CloudHSM, Fortanix HSM
- **Root Key Protection**: HSM encrypts Infisical's root encryption keys using hardware-protected keys
- **Enterprise Requirements**: Ideal for government, financial, and healthcare organizations
```bash
# HSM Environment Variables (example for production)
HSM_LIB_PATH="/path/to/hsm/library.so"
HSM_PIN="your-hsm-pin"
HSM_SLOT="0"
HSM_KEY_LABEL="infisical-root-key"
```
For complete HSM setup instructions, see the [HSM Integration Guide](/documentation/platform/kms/hsm-integration).
### External Key Management Service (KMS) Integration
Leverage cloud-native KMS providers for enhanced security and compliance:
Infisical can integrate with external KMS providers to encrypt project secrets, providing enterprise-grade key management:
- **Supported Providers**: AWS KMS, Google Cloud KMS, Azure Key Vault (coming soon)
- **Workspace Key Protection**: Each project's encryption key is protected by your external KMS
- **Envelope Encryption**: Infisical uses your cloud KMS to encrypt/decrypt project workspace keys, which in turn encrypt the actual secret data
- **Compliance**: Leverage your cloud provider's compliance certifications (FedRAMP, SOC2, ISO 27001)
#### Benefits for Production Deployments
- **Separation of Concerns**: Keys managed in your cloud infrastructure, separate from Infisical
- **Regulatory Compliance**: Use your existing compliance-certified KMS infrastructure
- **Audit Integration**: KMS operations logged in your cloud provider's audit trails
- **Disaster Recovery**: Keys backed by your cloud provider's HA and backup systems
- **Access Controls**: Leverage your cloud IAM for KMS access management
#### Configuration Resources
For external KMS configuration, see:
- [AWS KMS Integration](/documentation/platform/kms-configuration/aws-kms)
- [GCP KMS Integration](/documentation/platform/kms-configuration/gcp-kms)
- [External KMS Overview](/documentation/platform/kms-configuration/overview)
## Advanced Security Configurations
### Backup Security
**Configure backup encryption**. Encrypt PostgreSQL backups:
```bash
# PostgreSQL backup with encryption
pg_dump $DB_CONNECTION_URI | gpg --cipher-algo AES256 --compress-algo 1 --symmetric --output backup.sql.gpg
```
### Monitoring and Logging
**Implement log monitoring**. Set up centralized logging for security analysis and audit trails. Configure your SIEM or logging platform to monitor Infisical operations.
### Security Updates
**Regular security updates**. Monitor the [Infisical repository](https://github.com/Infisical/infisical) for security updates and apply them promptly.
## Compliance and Monitoring
### Enterprise Compliance Requirements
For enterprise deployments requiring compliance certifications:
- Implement audit log retention policies
- Set up security event monitoring and alerting
- Configure automated vulnerability scanning
- Establish incident response procedures
- Document security controls for compliance audits
### Standards Compliance
**FIPS 140-3 Compliance**. Infisical is actively working on FIPS 140-3 compliance to meet U.S. and Canadian government cryptographic standards. This will provide validated cryptographic modules for organizations requiring certified encryption implementations.

View File

@ -242,6 +242,7 @@ interface CreateIdentityEvent {
metadata: {
identityId: string;
name: string;
hasDeleteProtection: boolean;
};
}
@ -250,6 +251,7 @@ interface UpdateIdentityEvent {
metadata: {
identityId: string;
name?: string;
hasDeleteProtection?: boolean;
};
}

View File

@ -85,12 +85,13 @@ export const useCreateIdentity = () => {
export const useUpdateIdentity = () => {
const queryClient = useQueryClient();
return useMutation<Identity, object, UpdateIdentityDTO>({
mutationFn: async ({ identityId, name, role, metadata }) => {
mutationFn: async ({ identityId, name, role, hasDeleteProtection, metadata }) => {
const {
data: { identity }
} = await apiRequest.patch(`/api/v1/identities/${identityId}`, {
name,
role,
hasDeleteProtection,
metadata
});

View File

@ -14,6 +14,7 @@ export type IdentityTrustedIp = {
export type Identity = {
id: string;
name: string;
hasDeleteProtection: boolean;
authMethods: IdentityAuthMethod[];
createdAt: string;
updatedAt: string;
@ -83,6 +84,7 @@ export type CreateIdentityDTO = {
name: string;
organizationId: string;
role?: string;
hasDeleteProtection: boolean;
metadata?: { key: string; value: string }[];
};
@ -90,6 +92,7 @@ export type UpdateIdentityDTO = {
identityId: string;
name?: string;
role?: string;
hasDeleteProtection?: boolean;
organizationId: string;
metadata?: { key: string; value: string }[];
};

View File

@ -15,7 +15,8 @@ import {
IconButton,
Input,
Modal,
ModalContent
ModalContent,
Switch
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { findOrgMembershipRole } from "@app/helpers/roles";
@ -27,6 +28,7 @@ const schema = z
.object({
name: z.string().min(1, "Required"),
role: z.object({ slug: z.string(), name: z.string() }),
hasDeleteProtection: z.boolean(),
metadata: z
.object({
key: z.string().trim().min(1),
@ -64,7 +66,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: ""
name: "",
hasDeleteProtection: false
}
});
@ -78,6 +81,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
identityId: string;
name: string;
role: string;
hasDeleteProtection: boolean;
metadata?: { key: string; value: string }[];
customRole: {
name: string;
@ -91,22 +95,25 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
reset({
name: identity.name,
role: identity.customRole ?? findOrgMembershipRole(roles, identity.role),
hasDeleteProtection: identity.hasDeleteProtection,
metadata: identity.metadata
});
} else {
reset({
name: "",
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole)
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole),
hasDeleteProtection: false
});
}
}, [popUp?.identity?.data, roles]);
const onFormSubmit = async ({ name, role, metadata }: FormData) => {
const onFormSubmit = async ({ name, role, metadata, hasDeleteProtection }: FormData) => {
try {
const identity = popUp?.identity?.data as {
identityId: string;
name: string;
role: string;
hasDeleteProtection: boolean;
};
if (identity) {
@ -116,6 +123,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
identityId: identity.identityId,
name,
role: role.slug || undefined,
hasDeleteProtection,
organizationId: orgId,
metadata
});
@ -127,6 +135,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
const { id: createdId } = await createMutateAsync({
name,
role: role.slug || undefined,
hasDeleteProtection,
organizationId: orgId,
metadata
});
@ -215,6 +224,24 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl>
)}
/>
<Controller
control={control}
name="hasDeleteProtection"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Switch
className="ml-0 mr-2 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
containerClassName="flex-row-reverse w-fit"
id="delete-protection-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>Delete Protection {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
)}
/>
<div>
<FormLabel label="Metadata" />
</div>

View File

@ -329,7 +329,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="start" className="mt-3 p-1">
<OrgPermissionCan
I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity}

View File

@ -47,13 +47,19 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
<DropdownMenuTrigger asChild>
<Button
size="xs"
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
rightIcon={
<FontAwesomeIcon
className="ml-1 transition-transform duration-200 group-data-[state=open]:rotate-180"
icon={faChevronDown}
/>
}
colorSchema="secondary"
className="group select-none"
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[120px]" align="end">
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
<OrgPermissionCan
I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity}
@ -68,6 +74,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
handlePopUpOpen("identity", {
identityId,
name: data.identity.name,
hasDeleteProtection: data.identity.hasDeleteProtection,
role: data.role,
customRole: data.customRole
});
@ -131,6 +138,12 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Delete Protection</p>
<p className="text-sm text-mineshaft-300">
{data.identity.hasDeleteProtection ? "On" : "Off"}
</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
<p className="text-sm text-mineshaft-300">{data.role}</p>

View File

@ -35,7 +35,7 @@ export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[120px]" align="end">
<DropdownMenuContent className="mt-3 min-w-[120px]" align="end">
<OrgPermissionCan
I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity}

View File

@ -72,7 +72,7 @@ export const GroupsSection = () => {
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()}

View File

@ -2,10 +2,11 @@ import { useMemo } from "react";
import {
faArrowDown,
faArrowUp,
faEllipsisV,
faMagnifyingGlass,
faSearch,
faTrash,
faUsers
faUsers,
faUsersSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
@ -13,6 +14,10 @@ import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
@ -184,33 +189,42 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => (
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Remove">
<IconButton
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", {
id,
name
});
}}
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Groups}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUsersSlash} />}
isDisabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", {
id,
name
});
}}
>
Remove Group From Project
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);

View File

@ -3,12 +3,13 @@ import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faBookOpen,
faCircleXmark,
faClock,
faEllipsisV,
faMagnifyingGlass,
faPlus,
faServer,
faXmark
faServer
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
@ -21,6 +22,10 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
HoverCard,
HoverCardContent,
@ -163,20 +168,21 @@ export const IdentityTab = withProjectPermission(
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<div className="flex w-full justify-end pr-4">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/identities/overview"
>
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</span>
</div>
</a>
</div>
<ProjectPermissionCan
@ -185,7 +191,7 @@ export const IdentityTab = withProjectPermission(
>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("identity")}
@ -231,7 +237,7 @@ export const IdentityTab = withProjectPermission(
</Th>
<Th className="w-1/3">Role</Th>
<Th>Added on</Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
@ -372,37 +378,46 @@ export const IdentityTab = withProjectPermission(
</div>
</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end space-x-2 opacity-0 duration-300 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Identity, {
identityId: id
})}
>
{(isAllowed) => (
<IconButton
onClick={(evt) => {
evt.stopPropagation();
evt.preventDefault();
handlePopUpOpen("deleteIdentity", {
identityId: id,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
<IconButton ariaLabel="more-icon" variant="plain">
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
<Td className="flex justify-end space-x-2">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Identity, {
identityId: id
})}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCircleXmark} />}
isDisabled={!isAllowed}
onClick={(evt) => {
evt.stopPropagation();
evt.preventDefault();
handlePopUpOpen("deleteIdentity", {
identityId: id,
name
});
}}
>
Remove Identity From Project
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);

View File

@ -56,12 +56,12 @@ export const MembersSection = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<div className="mb-4 flex items-center justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Users</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addMember")}

View File

@ -9,8 +9,8 @@ import {
faFilter,
faMagnifyingGlass,
faSearch,
faTrash,
faUsers
faUsers,
faUserXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
@ -416,32 +416,40 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
</Td>
<Td>
{userId !== u?.id && (
<div className="flex items-center space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
colorSchema="danger"
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={userId === u?.id || !isAllowed}
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
handlePopUpOpen("removeMember", { username: u.username });
}}
>
<FontAwesomeIcon icon={faTrash} />
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
)}
</ProjectPermissionCan>
<IconButton ariaLabel="more-icon" variant="plain">
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserXmark} />}
isDisabled={!isAllowed}
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
handlePopUpOpen("removeMember", { username: u.username });
}}
>
Remove User From Project
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
)}
</Td>
</Tr>

View File

@ -10,10 +10,10 @@ export const ProjectRoleListTab = withProjectPermission(
return (
<motion.div
key="role-list"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: -30 }}
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectRoleList />
</motion.div>

View File

@ -1,4 +1,16 @@
import { faEllipsis, faPlus } from "@fortawesome/free-solid-svg-icons";
import { useMemo } from "react";
import {
faArrowDown,
faArrowUp,
faCopy,
faEdit,
faEllipsisV,
faEye,
faMagnifyingGlass,
faPlus,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
@ -12,6 +24,10 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
@ -19,15 +35,27 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types";
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
enum RolesOrderBy {
Name = "name",
Slug = "slug"
}
export const ProjectRoleList = () => {
const navigate = useNavigate();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@ -57,6 +85,75 @@ export const ProjectRoleList = () => {
}
};
const {
orderDirection,
toggleOrderDirection,
orderBy,
setOrderDirection,
setOrderBy,
search,
setSearch,
page,
perPage,
setPerPage,
setPage,
offset
} = usePagination<RolesOrderBy>(RolesOrderBy.Name, {
initPerPage: getUserTablePreference("projectRolesTable", PreferenceKey.PerPage, 20)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("projectRolesTable", PreferenceKey.PerPage, newPerPage);
};
const filteredRoles = useMemo(
() =>
roles
?.filter((role) => {
const { slug, name } = role;
const searchValue = search.trim().toLowerCase();
return (
name.toLowerCase().includes(searchValue) || slug.toLowerCase().includes(searchValue)
);
})
.sort((a, b) => {
const [roleOne, roleTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
switch (orderBy) {
case RolesOrderBy.Slug:
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
case RolesOrderBy.Name:
default:
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
}
}) ?? [],
[roles, orderDirection, search, orderBy]
);
useResetPageHelper({
totalCount: filteredRoles.length,
offset,
setPage
});
const handleSort = (column: RolesOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
const getClassName = (col: RolesOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
const getColSortIcon = (col: RolesOrderBy) =>
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
@ -64,7 +161,7 @@ export const ProjectRoleList = () => {
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
{(isAllowed) => (
<Button
colorSchema="primary"
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("role")}
@ -75,18 +172,50 @@ export const ProjectRoleList = () => {
)}
</ProjectPermissionCan>
</div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search project roles..."
className="flex-1"
containerClassName="mb-4"
/>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Name)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Name)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Name)} />
</IconButton>
</div>
</Th>
<Th>
<div className="flex items-center">
Slug
<IconButton
variant="plain"
className={getClassName(RolesOrderBy.Slug)}
ariaLabel="sort"
onClick={() => handleSort(RolesOrderBy.Slug)}
>
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Slug)} />
</IconButton>
</div>
</Th>
<Th aria-label="actions" className="w-5" />
</Tr>
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{roles?.map((role) => {
{isRolesLoading && <TableSkeleton columns={3} innerKey="project-roles" />}
{filteredRoles?.slice(offset, perPage * page).map((role) => {
const { id, name, slug } = role;
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
slug as ProjectMembershipRole
@ -109,88 +238,118 @@ export const ProjectRoleList = () => {
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
navigate({
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
params: {
projectId: currentWorkspace.id,
roleSlug: slug
}
});
}}
disabled={!isAllowed}
>
{`${isNonMutatable ? "View" : "Edit"} Role`}
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("duplicateRole", role);
}}
disabled={!isAllowed}
>
Duplicate Role
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{!isNonMutatable && (
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[12rem]" sideOffset={2} align="end">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isNonMutatable ? faEye : faEdit} />}
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
navigate({
to: `/${currentWorkspace?.type}/$projectId/roles/$roleSlug` as const,
params: {
projectId: currentWorkspace.id,
roleSlug: slug
}
});
}}
disabled={!isAllowed}
>
Delete Role
{`${isNonMutatable ? "View" : "Edit"} Role`}
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
</DropdownMenuContent>
</DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCopy} />}
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("duplicateRole", role);
}}
disabled={!isAllowed}
>
Duplicate Role
</DropdownMenuItem>
)}
</ProjectPermissionCan>
{!isNonMutatable && (
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Role}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faTrash} />}
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50",
"transition-colors duration-100"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteRole", role);
}}
disabled={!isAllowed}
>
Delete Role
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{Boolean(filteredRoles?.length) && (
<Pagination
count={filteredRoles!.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!filteredRoles?.length && !isRolesLoading && (
<EmptyState
title={
roles?.length
? "No project roles match search..."
: "This project does not have any roles"
}
icon={roles?.length ? faSearch : undefined}
/>
)}
</TableContainer>
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal

View File

@ -96,6 +96,10 @@ import { SecretOverviewSecretRotationRow } from "@app/pages/secret-manager/Overv
import { CreateDynamicSecretForm } from "../SecretDashboardPage/components/ActionBar/CreateDynamicSecretForm";
import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm";
import {
HIDDEN_SECRET_VALUE,
HIDDEN_SECRET_VALUE_API_MASK
} from "../SecretDashboardPage/components/SecretListView/SecretItem";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
@ -509,15 +513,25 @@ export const OverviewPage = () => {
env: string,
key: string,
value: string,
secretValueHidden: boolean,
type = SecretType.Shared
) => {
let secretValue: string | undefined = value;
if (
secretValueHidden &&
(value === HIDDEN_SECRET_VALUE_API_MASK || value === HIDDEN_SECRET_VALUE)
) {
secretValue = undefined;
}
try {
const result = await updateSecretV3({
environment: env,
workspaceId,
secretPath,
secretKey: key,
secretValue: value,
secretValue,
type
});

View File

@ -50,6 +50,7 @@ type Props = {
env: string,
key: string,
value: string,
secretValueHidden: boolean,
type?: SecretType,
secretId?: string
) => Promise<void>;
@ -147,6 +148,7 @@ export const SecretEditRow = ({
environment,
secretName,
value,
secretValueHidden,
isOverride ? SecretType.Personal : SecretType.Shared,
secretId
);
@ -166,6 +168,7 @@ export const SecretEditRow = ({
environment,
secretName,
secretValue,
secretValueHidden,
isOverride ? SecretType.Personal : SecretType.Shared,
secretId
);
@ -257,7 +260,7 @@ export const SecretEditRow = ({
>
{(isAllowed) => (
<div>
<Tooltip content="save">
<Tooltip content="Save">
<IconButton
variant="plain"
ariaLabel="submit-value"

View File

@ -24,6 +24,7 @@ import { useToggle } from "@app/hooks";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { getExpandedRowStyle } from "@app/pages/secret-manager/OverviewPage/components/utils";
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
import { SecretEditRow } from "./SecretEditRow";
import SecretRenameRow from "./SecretRenameRow";
@ -40,6 +41,7 @@ type Props = {
env: string,
key: string,
value: string,
secretValueHidden: boolean,
type?: SecretType,
secretId?: string
) => Promise<void>;
@ -96,7 +98,7 @@ export const SecretOverviewTableRow = ({
);
if (secret?.secretValueHidden && !secret?.valueOverride) {
return canEditSecretValue ? "******" : "";
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
}
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
};

View File

@ -62,6 +62,7 @@ import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { camelCaseToSpaces } from "@app/lib/fn/string";
import { CreateReminderForm } from "./CreateReminderForm";
import { HIDDEN_SECRET_VALUE } from "./SecretItem";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
type Props = {
@ -897,7 +898,9 @@ export const SecretDetailSidebar = ({
</button>
</div>
<span className="group-[.show-value]:hidden">
{secretValueHidden ? "******" : secretValue?.replace(/./g, "*")}
{secretValueHidden
? HIDDEN_SECRET_VALUE
: secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"

View File

@ -59,6 +59,7 @@ import {
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
export const HIDDEN_SECRET_VALUE = "******";
export const HIDDEN_SECRET_VALUE_API_MASK = "<hidden-by-infisical>";
type Props = {
secret: SecretV3RawSanitized;

View File

@ -20,7 +20,7 @@ import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/c
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
import { SecretDetailSidebar } from "./SecretDetailSidebar";
import { SecretItem } from "./SecretItem";
import { HIDDEN_SECRET_VALUE, HIDDEN_SECRET_VALUE_API_MASK, SecretItem } from "./SecretItem";
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
type Props = {
@ -85,6 +85,7 @@ export const SecretListView = ({
type: SecretType,
key: string,
{
secretValueHidden,
value,
comment,
reminderRepeatDays,
@ -97,6 +98,7 @@ export const SecretListView = ({
secretMetadata,
isRotatedSecret
}: Partial<{
secretValueHidden: boolean;
value: string;
comment: string;
reminderRepeatDays: number | null;
@ -123,6 +125,15 @@ export const SecretListView = ({
}
if (operation === "update") {
let secretValue = value;
if (
secretValueHidden &&
(value === HIDDEN_SECRET_VALUE_API_MASK || value === HIDDEN_SECRET_VALUE)
) {
secretValue = undefined;
}
await updateSecretV3({
environment,
workspaceId,
@ -130,7 +141,7 @@ export const SecretListView = ({
secretKey: key,
...(!isRotatedSecret && {
newSecretName: newKey,
secretValue: value || ""
secretValue: secretValueHidden ? secretValue : secretValue || ""
}),
type,
tagIds: tags,
@ -168,7 +179,7 @@ export const SecretListView = ({
},
cb?: () => void
) => {
const { key: oldKey } = orgSecret;
const { key: oldKey, secretValueHidden } = orgSecret;
const {
key,
value,
@ -246,7 +257,8 @@ export const SecretListView = ({
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding,
secretMetadata,
isRotatedSecret: orgSecret.isRotatedSecret
isRotatedSecret: orgSecret.isRotatedSecret,
secretValueHidden
});
if (cb) cb();
}