Merge branch 'main' into fix/docs-connections-tab-title

This commit is contained in:
x032205
2025-11-14 16:01:00 -05:00
126 changed files with 3763 additions and 1123 deletions

View File

@@ -1,7 +1,7 @@
# Keys
# Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
ENCRYPTION_KEY=VVHnGZ0w98WLgISK4XSJcagezuG6EWRFTk48KE4Y5Mw=
# JWT
# Required secrets to sign JWT tokens

View File

@@ -1,2 +1,2 @@
DB_CONNECTION_URI=
DB_CONNECTION_URI=postgres://infisical:infisical@localhost:5432/infisical
AUDIT_LOGS_DB_CONNECTION_URI=

View File

@@ -20,6 +20,4 @@
---
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/getting-started/code-of-conduct). 📝
<!-- If you have any questions regarding contribution, here's the FAQ : https://infisical.com/docs/contributing/getting-started/faq -->
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/getting-started/code-of-conduct). 📝

View File

@@ -49,7 +49,14 @@ jobs:
run: |
cp .env.example .env
echo "ACME_DEVELOPMENT_MODE=true" >> .env
echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env
echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\", \"infisical.com\": \"host.docker.internal:8087\", \"example.com\": \"host.docker.internal:8087\"}" >> .env
echo "BDD_NOCK_API_ENABLED=true" >> .env
# Skip upstream validation, otherwise the ACME client for the upstream will try to
# validate the DNS records, which will fail because the DNS records are not actually created.
echo "ACME_SKIP_UPSTREAM_VALIDATION=true" >> .env
# We are not using FIPS mode, need a different encryption key for BDD tests
NEW_ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
sed -i "s#ENCRYPTION_KEY=.*#ENCRYPTION_KEY=$NEW_ENCRYPTION_KEY#" .env
# Enable ACME feature in license for BDD tests
sed -i 's/pkiAcme: .*/pkiAcme: true,/g' backend/src/ee/services/license/license-fns.ts
- name: Set up Docker Buildx

View File

@@ -87,7 +87,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
## Getting started
Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/introduction)
Check out the [Quickstart Guides](https://infisical.com/docs/documentation/getting-started/overview)
| Use Infisical Cloud | Deploy Infisical on premise |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |

View File

@@ -2,16 +2,21 @@ import json
import os
import pathlib
import typing
import httpx
from behave.runner import Context
from dotenv import load_dotenv
from faker import Faker
import logging
from features.steps.utils import clean_all_nock, restore_nock
load_dotenv()
logger = logging.getLogger(__name__)
BASE_URL = os.environ.get("INFISICAL_API_URL", "http://localhost:8080")
PEBBLE_URL = os.environ.get("PEBBLE_URL", "https://pebble:14000/dir")
PROJECT_ID = os.environ.get("PROJECT_ID")
CERT_CA_ID = os.environ.get("CERT_CA_ID")
CERT_TEMPLATE_ID = os.environ.get("CERT_TEMPLATE_ID")
@@ -116,7 +121,7 @@ def bootstrap_infisical(context: Context):
"name": cert_template_slug,
"description": "",
"subject": [{"type": "common_name", "allowed": ["*"]}],
"sans": [],
"sans": [{"type": "dns_name", "allowed": ["*"]}],
"keyUsages": {
"required": [],
"allowed": [
@@ -184,6 +189,7 @@ def before_all(context: Context):
details = bootstrap_infisical(context)
context.vars = {
"BASE_URL": BASE_URL,
"PEBBLE_URL": PEBBLE_URL,
"PROJECT_ID": details["project"]["id"],
"CERT_CA_ID": details["ca"]["id"],
"CERT_TEMPLATE_ID": details["cert_template"]["id"],
@@ -192,9 +198,17 @@ def before_all(context: Context):
else:
context.vars = {
"BASE_URL": BASE_URL,
"PEBBLE_URL": PEBBLE_URL,
"PROJECT_ID": PROJECT_ID,
"CERT_CA_ID": CERT_CA_ID,
"CERT_TEMPLATE_ID": CERT_TEMPLATE_ID,
"AUTH_TOKEN": AUTH_TOKEN,
}
context.http_client = httpx.Client(base_url=BASE_URL)
def after_scenario(context: Context, scenario: typing.Any):
if hasattr(context, "web_server"):
context.web_server.shutdown_and_server_close()
clean_all_nock(context)
restore_nock(context)

View File

@@ -14,8 +14,195 @@ Feature: Challenge
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I select challenge with type http-01 for domain localhost from order at order as challenge
And I select challenge with type http-01 for domain localhost from order in order as challenge
And I serve challenge response for challenge at localhost
And I tell ACME server that challenge is ready to be verified
And I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq ".subject.common_name" should be equal to "localhost"
Scenario: Validate challenges for multiple domains
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
And I add subject alternative name to certificate signing request csr
"""
[
"infisical.com",
"example.com"
]
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I pass all challenges with type http-01 for order in order
And I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq ".subject.common_name" should be equal to "localhost"
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
"""
[
"example.com",
"infisical.com"
]
"""
Scenario: Did not finish all challenges
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
And I add subject alternative name to certificate signing request csr
"""
[
"infisical.com"
]
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I select challenge with type http-01 for domain localhost from order in order as challenge
And I serve challenge response for challenge at localhost
And I tell ACME server that challenge is ready to be verified
# the localhost auth should be valid
And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "localhost")) | first | .uri" as localhost_auth
And I peak and memorize the next nonce as nonce
When I send a raw ACME request to "{localhost_auth}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{localhost_auth}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "valid"
# the infisical.com auth should still be pending
And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "infisical.com")) | first | .uri" as infisical_auth
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
When I send a raw ACME request to "{infisical_auth}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{infisical_auth}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "pending"
# the order should be pending as well
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
When I send a raw ACME request to "{order.uri}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{order.uri}",
"kid": "{acme_account.uri}"
}
}
"""
Then the value response.status_code should be equal to 200
And the value response with jq ".status" should be equal to "pending"
# finalize should not be allowed when all auths are not valid yet
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
When I send a raw ACME request to "{order.body.finalize}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{order.body.finalize}",
"kid": "{acme_account.uri}"
},
"payload": {
"csr": "{csr_pem}"
}
}
"""
Then the value response.status_code should be equal to 400
Then the value response with jq ".status" should be equal to 400
Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:orderNotReady"
Then the value response with jq ".detail" should be equal to "ACME order is not ready"
Scenario: CSR names mismatch with order identifier
Given I have an ACME cert profile as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "example.com"
}
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
Then I peak and memorize the next nonce as nonce
When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order",
"kid": "{acme_account.uri}"
},
"payload": {
"identifiers": [
{ "type": "dns", "value": "localhost" },
{ "type": "dns", "value": "infisical.com" }
]
}
}
"""
Then the value response.status_code should be equal to 201
And I memorize response with jq ".finalize" as finalize_url
And I memorize response.headers with jq ".["replay-nonce"]" as nonce
And I memorize response as order
And I pass all challenges with type http-01 for order in order
And I encode CSR csr_pem as JOSE Base-64 DER as base64_csr_der
When I send a raw ACME request to "{finalize_url}"
"""
{
"protected": {
"alg": "RS256",
"nonce": "{nonce}",
"url": "{finalize_url}",
"kid": "{acme_account.uri}"
},
"payload": {
"csr": "{base64_csr_der}"
}
}
"""
Then the value response.status_code should be equal to 400
And the value response with jq ".status" should be equal to 400
And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badCSR"
And the value response with jq ".detail" should be equal to "Invalid CSR: Common name + SANs mismatch with order identifiers"

View File

@@ -0,0 +1,180 @@
Feature: External CA
Scenario: Issue a certificate from an external CA
Given I create a Cloudflare connection as cloudflare
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
Given I create a external ACME CA with the following config as ext_ca
"""
{
"dnsProviderConfig": {
"provider": "cloudflare",
"hostedZoneId": "MOCK_ZONE_ID"
},
"directoryUrl": "{PEBBLE_URL}",
"accountEmail": "fangpen@infisical.com",
"dnsAppConnectionId": "{app_conn_id}",
"eabKid": "",
"eabHmacKey": ""
}
"""
Then I memorize ext_ca with jq ".id" as ext_ca_id
Given I create a certificate template with the following config as cert_template
"""
{
"subject": [
{
"type": "common_name",
"allowed": [
"*"
]
}
],
"sans": [
{
"type": "dns_name",
"allowed": [
"*"
]
}
],
"keyUsages": {
"required": [],
"allowed": [
"digital_signature",
"key_encipherment",
"non_repudiation",
"data_encipherment",
"key_agreement",
"key_cert_sign",
"crl_sign",
"encipher_only",
"decipher_only"
]
},
"extendedKeyUsages": {
"required": [],
"allowed": [
"client_auth",
"server_auth",
"code_signing",
"email_protection",
"ocsp_signing",
"time_stamping"
]
},
"algorithms": {
"signature": [
"SHA256-RSA",
"SHA512-RSA",
"SHA384-ECDSA",
"SHA384-RSA",
"SHA256-ECDSA",
"SHA512-ECDSA"
],
"keyAlgorithm": [
"RSA-2048",
"RSA-4096",
"ECDSA-P384",
"RSA-3072",
"ECDSA-P256",
"ECDSA-P521"
]
},
"validity": {
"max": "365d"
}
}
"""
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
{
"COMMON_NAME": "localhost"
}
"""
# Pebble has a strict rule to only takes SANs
Then I add subject alternative name to certificate signing request csr
"""
[
"localhost"
]
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I select challenge with type http-01 for domain localhost from order in order as challenge
And I serve challenge response for challenge at localhost
And I tell ACME server that challenge is ready to be verified
Given I intercept outgoing requests
"""
[
{
"scope": "https://api.cloudflare.com:443",
"method": "POST",
"path": "/client/v4/zones/MOCK_ZONE_ID/dns_records",
"status": 200,
"response": {
"result": {
"id": "A2A6347F-88B5-442D-9798-95E408BC7701",
"name": "Mock Account",
"type": "standard",
"settings": {
"enforce_twofactor": false,
"api_access_enabled": null,
"access_approval_expiry": null,
"abuse_contact_email": null,
"user_groups_ui_beta": false
},
"legacy_flags": {
"enterprise_zone_quota": {
"maximum": 0,
"current": 0,
"available": 0
}
},
"created_on": "2013-04-18T00:41:02.215243Z"
},
"success": true,
"errors": [],
"messages": []
},
"responseIsBinary": false
},
{
"scope": "https://api.cloudflare.com:443",
"method": "GET",
"path": {
"regex": "/client/v4/zones/[^/]+/dns_records\\?"
},
"status": 200,
"response": {
"result": [],
"success": true,
"errors": [],
"messages": [],
"result_info": {
"page": 1,
"per_page": 100,
"count": 0,
"total_count": 0,
"total_pages": 1
}
},
"responseIsBinary": false
}
]
"""
Then I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
# Note: somehow Pebble is issuing a cert without common name but just SANs
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
"""
[
"localhost"
]
"""

View File

@@ -1,14 +1,10 @@
import json
import logging
import re
import threading
import urllib.parse
import acme.client
import httpx
import jq
import requests
import glom
from faker import Faker
from acme import client
from acme import messages
@@ -19,7 +15,6 @@ from behave import given
from behave import when
from behave import then
from josepy.jwk import JWKRSA
from josepy import JSONObjectWithFields
from josepy import json_util
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
@@ -27,6 +22,12 @@ from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from features.steps.utils import define_nock, clean_all_nock, restore_nock
from utils import replace_vars, with_nocks
from utils import eval_var
from utils import prepare_headers
ACC_KEY_BITS = 2048
ACC_KEY_PUBLIC_EXPONENT = 65537
logger = logging.getLogger(__name__)
@@ -40,96 +41,6 @@ class AcmeProfile:
self.eab_secret = eab_secret
def replace_vars(payload: dict | list | int | float | str, vars: dict):
if isinstance(payload, dict):
return {
replace_vars(key, vars): replace_vars(value, vars)
for key, value in payload.items()
}
elif isinstance(payload, list):
return [replace_vars(item, vars) for item in payload]
elif isinstance(payload, str):
return payload.format(**vars)
else:
return payload
def parse_glom_path(path_str: str) -> glom.Path:
"""
Parse a glom path string with 'attr[index]' syntax into a Path object.
Examples:
>>> parse_glom_path('authorizations[0]') == Path('authorizations', 0)
True
>>> parse_glom_path('data.items[1].name') == Path('data', 'items', 1, 'name')
True
>>> parse_glom_path('user.addresses[0].street') == Path('user', 'addresses', 0, 'street')
True
"""
parts = []
# Split by dots, but preserve bracketed content
tokens = re.split(r"(?<!\[)\.(?![^\[]*\])", path_str)
for token in tokens:
token = token.strip()
if not token:
continue
# Check for attr[index] pattern
match = re.match(r"^(.+?)\[([^\]]+)\]$", token)
if match:
attr_name = match.group(1).strip()
index_str = match.group(2).strip()
# Parse index (support integers, slices, etc.)
if index_str.isdigit():
index = int(index_str)
elif "-" in index_str:
# Handle negative indices like [-1]
index = int(index_str)
elif ":" in index_str:
# Handle slices like [0:10]
index = slice(
*map(int, [x.strip() for x in index_str.split(":") if x.strip()])
)
else:
# Treat as string key
index = index_str
parts.extend([attr_name, index])
else:
# Plain attribute/key
parts.append(token)
return glom.Path(*parts)
def eval_var(context: Context, var_path: str, as_json: bool = True):
parts = var_path.split(".", 1)
value = context.vars[parts[0]]
if len(parts) == 2:
value = glom.glom(value, parse_glom_path(parts[1]))
if as_json:
if isinstance(value, JSONObjectWithFields):
value = value.to_json()
elif isinstance(value, requests.Response):
value = value.json()
elif isinstance(value, httpx.Response):
value = value.json()
return value
def prepare_headers(context: Context) -> dict | None:
headers = {}
auth_token = getattr(context, "auth_token", None)
if auth_token is not None:
headers["authorization"] = "Bearer {}".format(auth_token)
if not headers:
return None
return headers
@given("I make a random {faker_type} as {var_name}")
def step_impl(context: Context, faker_type: str, var_name: str):
context.vars[var_name] = getattr(faker, faker_type)()
@@ -177,6 +88,198 @@ def step_impl(context: Context, profile_var: str):
)
@given("I create a Cloudflare connection as {var_name}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]
conn_slug = faker.slug()
mock_account_id = "MOCK_ACCOUNT_ID"
with with_nocks(
context,
definitions=[
{
"scope": "https://api.cloudflare.com:443",
"method": "GET",
"path": f"/client/v4/accounts/{mock_account_id}",
"status": 200,
"response": {
"result": {
"id": "A2A6347F-88B5-442D-9798-95E408BC7701",
"name": "Mock Account",
"type": "standard",
"settings": {
"enforce_twofactor": True,
"api_access_enabled": None,
"access_approval_expiry": None,
"abuse_contact_email": None,
"user_groups_ui_beta": False,
},
"legacy_flags": {
"enterprise_zone_quota": {
"maximum": 0,
"current": 0,
"available": 0,
}
},
"created_on": "2013-04-18T00:41:02.215243Z",
},
"success": True,
"errors": [],
"messages": [],
},
"responseIsBinary": False,
}
],
):
response = context.http_client.post(
"/api/v1/app-connections/cloudflare",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"name": conn_slug,
"description": "",
"method": "api-token",
"credentials": {
"apiToken": "MOCK_API_TOKEN",
"accountId": mock_account_id,
},
},
)
response.raise_for_status()
context.vars[var_name] = response
@given("I create a external ACME CA with the following config as {var_name}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]
ca_slug = faker.slug()
config = replace_vars(json.loads(context.text), context.vars)
response = context.http_client.post(
"/api/v1/pki/ca/acme",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"name": ca_slug,
"type": "acme",
"status": "active",
"enableDirectIssuance": True,
"configuration": config,
},
)
response.raise_for_status()
context.vars[var_name] = response
@given("I create a certificate template with the following config as {var_name}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]
template_slug = faker.slug()
config = replace_vars(json.loads(context.text), context.vars)
response = context.http_client.post(
"/api/v2/certificate-templates",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"name": template_slug,
"description": "",
}
| config,
)
response.raise_for_status()
context.vars[var_name] = response
@given(
'I create an ACME profile with ca {ca_id} and template {template_id} as "{profile_var}"'
)
def step_impl(context: Context, ca_id: str, template_id: str, profile_var: str):
profile_slug = faker.slug()
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/pki/certificate-profiles",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"slug": profile_slug,
"description": "ACME Profile created by BDD test",
"enrollmentType": "acme",
"caId": replace_vars(ca_id, context.vars),
"certificateTemplateId": replace_vars(template_id, context.vars),
"acmeConfig": {},
},
)
response.raise_for_status()
resp_json = response.json()
profile_id = resp_json["certificateProfile"]["id"]
kid = profile_id
response = context.http_client.get(
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
headers=dict(authorization="Bearer {}".format(jwt_token)),
)
response.raise_for_status()
resp_json = response.json()
secret = resp_json["eabSecret"]
context.vars[profile_var] = AcmeProfile(
profile_id,
eab_kid=kid,
eab_secret=secret,
)
@given('I have an ACME cert profile with external ACME CA as "{profile_var}"')
def step_impl(context: Context, profile_var: str):
profile_id = context.vars.get("PROFILE_ID")
secret = context.vars.get("EAB_SECRET")
if profile_id is not None and secret is not None:
kid = profile_id
else:
profile_slug = faker.slug()
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/pki/certificate-profiles",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"projectId": context.vars["PROJECT_ID"],
"slug": profile_slug,
"description": "ACME Profile created by BDD test",
"enrollmentType": "acme",
"caId": context.vars["CERT_CA_ID"],
"certificateTemplateId": context.vars["CERT_TEMPLATE_ID"],
"acmeConfig": {},
},
)
response.raise_for_status()
resp_json = response.json()
profile_id = resp_json["certificateProfile"]["id"]
kid = profile_id
response = context.http_client.get(
f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
headers=dict(authorization="Bearer {}".format(jwt_token)),
)
response.raise_for_status()
resp_json = response.json()
secret = resp_json["eabSecret"]
context.vars[profile_var] = AcmeProfile(
profile_id,
eab_kid=kid,
eab_secret=secret,
)
@given("I intercept outgoing requests")
def step_impl(context: Context):
definitions = replace_vars(json.loads(context.text), context.vars)
define_nock(context, definitions)
@then("I reset requests interceptions")
def step_impl(context: Context):
clean_all_nock(context)
restore_nock(context)
@given("I use {token_var} for authentication")
def step_impl(context: Context, token_var: str):
context.auth_token = eval_var(context, token_var)
@@ -387,6 +490,15 @@ def step_impl(context: Context, url: str):
send_raw_acme_req(context, url)
@then(
"I encode CSR {pem_var} as JOSE Base-64 DER as {var_name}",
)
def step_impl(context: Context, pem_var: str, var_name: str):
csr = eval_var(context, pem_var)
parsed_csr = x509.load_pem_x509_csr(csr)
context.vars[var_name] = json_util.encode_csr(parsed_csr)
@then(
"I submit the certificate signing request PEM {pem_var} certificate order to the ACME server as {order_var}"
)
@@ -569,51 +681,61 @@ def step_impl(context: Context, var_path: str):
print(json.dumps(value.json(), indent=2))
@then(
"I select challenge with type {challenge_type} for domain {domain} from order at {var_path} as {challenge_var}"
)
def step_impl(
def select_challenge(
context: Context,
challenge_type: str,
order_var_path: str,
domain: str,
var_path: str,
challenge_var: str,
):
order = eval_var(context, var_path, as_json=False)
acme_client = context.acme_client
order = eval_var(context, order_var_path, as_json=False)
if isinstance(order, dict):
order_body = messages.Order.from_json(order)
order = messages.OrderResource(
body=order_body,
authorizations=[
acme_client._authzr_from_response(
acme_client._post_as_get(url), uri=url
)
for url in order_body.authorizations
],
)
if not isinstance(order, messages.OrderResource):
raise ValueError(
f"Expected OrderResource but got {type(order)!r} at {var_path!r}"
f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}"
)
auths = list(
filter(lambda o: o.body.identifier.value == domain, order.authorizations)
)
if not auths:
raise ValueError(
f"Authorization for domain {domain!r} not found in {var_path!r}"
f"Authorization for domain {domain!r} not found in {order_var_path!r}"
)
if len(auths) > 1:
raise ValueError(
f"More than one order for domain {domain!r} found in {var_path!r}"
f"More than one order for domain {domain!r} found in {order_var_path!r}"
)
auth = auths[0]
challenges = list(filter(lambda a: a.typ == challenge_type, auth.body.challenges))
if not challenges:
raise ValueError(
f"Authorization type {challenge_type!r} not found in {var_path!r}"
f"Authorization type {challenge_type!r} not found in {order_var_path!r}"
)
if len(challenges) > 1:
raise ValueError(
f"More than one authorization for type {challenge_type!r} found in {var_path!r}"
f"More than one authorization for type {challenge_type!r} found in {order_var_path!r}"
)
context.vars[challenge_var] = challenges[0]
return challenges[0]
@then("I serve challenge response for {var_path} at {hostname}")
def step_impl(context: Context, var_path: str, hostname: str):
if hostname != "localhost":
raise ValueError("Currently only localhost is supported")
challenge = eval_var(context, var_path, as_json=False)
def serve_challenge(
context: Context,
challenge: messages.ChallengeBody,
):
if hasattr(context, "web_server"):
context.web_server.shutdown_and_server_close()
response, validation = challenge.response_and_validation(
context.acme_client.net.key
)
@@ -622,19 +744,101 @@ def step_impl(context: Context, var_path: str, hostname: str):
)
# TODO: make port configurable
servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource})
# Start client standalone web server.
web_server = threading.Thread(name="web_server", target=servers.serve_forever)
web_server.daemon = True
web_server.start()
context.web_server = web_server
servers.serve_forever()
context.web_server = servers
def notify_challenge_ready(context: Context, challenge: messages.ChallengeBody):
acme_client = context.acme_client
response, validation = challenge.response_and_validation(acme_client.net.key)
acme_client.answer_challenge(challenge, response)
@then(
"I select challenge with type {challenge_type} for domain {domain} from order in {var_path} as {challenge_var}"
)
def step_impl(
context: Context,
challenge_type: str,
domain: str,
var_path: str,
challenge_var: str,
):
challenge = select_challenge(
context=context,
challenge_type=challenge_type,
domain=domain,
order_var_path=var_path,
)
context.vars[challenge_var] = challenge
@then("I pass all challenges with type {challenge_type} for order in {order_var_path}")
def step_impl(
context: Context,
challenge_type: str,
order_var_path: str,
):
acme_client = context.acme_client
order = eval_var(context, order_var_path, as_json=False)
if isinstance(order, dict):
order_body = messages.Order.from_json(order)
order = messages.OrderResource(
body=order_body,
authorizations=[
acme_client._authzr_from_response(
acme_client._post_as_get(url), uri=url
)
for url in order_body.authorizations
],
)
if not isinstance(order, messages.OrderResource):
raise ValueError(
f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}"
)
for domain in order.body.identifiers:
logger.info(
"Selecting challenge for domain %s with type %s ...",
domain.value,
challenge_type,
)
challenge = select_challenge(
context=context,
challenge_type=challenge_type,
domain=domain.value,
order_var_path=order_var_path,
)
logger.info(
"Found challenge for domain %s with type %s, challenge=%s",
domain.value,
challenge_type,
challenge.uri,
)
logger.info(
"Serving challenge for domain %s with type %s ...",
domain.value,
challenge_type,
)
serve_challenge(context=context, challenge=challenge)
logger.info(
"Notifying challenge for domain %s with type %s ...", domain, challenge_type
)
notify_challenge_ready(context=context, challenge=challenge)
@then("I serve challenge response for {var_path} at {hostname}")
def step_impl(context: Context, var_path: str, hostname: str):
challenge = eval_var(context, var_path, as_json=False)
serve_challenge(context=context, challenge=challenge)
@then("I tell ACME server that {var_path} is ready to be verified")
def step_impl(context: Context, var_path: str):
challenge = eval_var(context, var_path, as_json=False)
acme_client = context.acme_client
response, validation = challenge.response_and_validation(acme_client.net.key)
acme_client.answer_challenge(challenge, response)
notify_challenge_ready(context=context, challenge=challenge)
@then("I poll and finalize the ACME order {var_path} as {finalized_var}")
@@ -643,3 +847,10 @@ def step_impl(context: Context, var_path: str, finalized_var: str):
acme_client = context.acme_client
finalized_order = acme_client.poll_and_finalize(order)
context.vars[finalized_var] = finalized_order
@then("I parse the full-chain certificate from order {order_var_path} as {cert_var}")
def step_impl(context: Context, order_var_path: str, cert_var: str):
order = eval_var(context, order_var_path, as_json=False)
cert = x509.load_pem_x509_certificate(order.fullchain_pem.encode())
context.vars[cert_var] = cert

View File

@@ -0,0 +1,302 @@
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.x509.oid import NameOID
import logging
import re
import contextlib
import httpx
import requests
import requests.structures
import glom
from faker import Faker
from behave.runner import Context
from josepy import JSONObjectWithFields
ACC_KEY_BITS = 2048
ACC_KEY_PUBLIC_EXPONENT = 65537
logger = logging.getLogger(__name__)
faker = Faker()
class AcmeProfile:
def __init__(self, id: str, eab_kid: str, eab_secret: str):
self.id = id
self.eab_kid = eab_kid
self.eab_secret = eab_secret
def replace_vars(payload: dict | list | int | float | str, vars: dict):
if isinstance(payload, dict):
return {
replace_vars(key, vars): replace_vars(value, vars)
for key, value in payload.items()
}
elif isinstance(payload, list):
return [replace_vars(item, vars) for item in payload]
elif isinstance(payload, str):
return payload.format(**vars)
else:
return payload
def parse_glom_path(path_str: str) -> glom.Path:
"""
Parse a glom path string with 'attr[index]' syntax into a Path object.
Examples:
>>> parse_glom_path('authorizations[0]') == Path('authorizations', 0)
True
>>> parse_glom_path('data.items[1].name') == Path('data', 'items', 1, 'name')
True
>>> parse_glom_path('user.addresses[0].street') == Path('user', 'addresses', 0, 'street')
True
"""
parts = []
# Split by dots, but preserve bracketed content
tokens = re.split(r"(?<!\[)\.(?![^\[]*\])", path_str)
for token in tokens:
token = token.strip()
if not token:
continue
# Check for attr[index] pattern
match = re.match(r"^(.+?)\[([^\]]+)\]$", token)
if match:
attr_name = match.group(1).strip()
index_str = match.group(2).strip()
# Parse index (support integers, slices, etc.)
if index_str.isdigit():
index = int(index_str)
elif "-" in index_str:
# Handle negative indices like [-1]
index = int(index_str)
elif ":" in index_str:
# Handle slices like [0:10]
index = slice(
*map(int, [x.strip() for x in index_str.split(":") if x.strip()])
)
else:
# Treat as string key
index = index_str
parts.extend([attr_name, index])
else:
# Plain attribute/key
parts.append(token)
return glom.Path(*parts)
def eval_var(context: Context, var_path: str, as_json: bool = True):
parts = var_path.split(".", 1)
value = context.vars[parts[0]]
if len(parts) == 2:
value = glom.glom(value, parse_glom_path(parts[1]))
if as_json:
if isinstance(value, JSONObjectWithFields):
value = value.to_json()
elif isinstance(value, requests.Response):
value = value.json()
elif isinstance(value, requests.structures.CaseInsensitiveDict):
value = dict(value.lower_items())
elif isinstance(value, httpx.Response):
value = value.json()
elif isinstance(value, x509.Certificate):
value = x509_cert_to_dict(value)
return value
def prepare_headers(context: Context) -> dict | None:
headers = {}
auth_token = getattr(context, "auth_token", None)
if auth_token is not None:
headers["authorization"] = "Bearer {}".format(auth_token)
if not headers:
return None
return headers
def x509_cert_to_dict(cert: x509.Certificate) -> dict:
"""
Convert a cryptography.x509.Certificate to a JSON-serializable nested dict
with human-readable keys.
"""
def oid_to_name(oid):
# Map known OIDs to human-readable names
mapping = {
NameOID.COMMON_NAME: "common_name",
NameOID.ORGANIZATION_NAME: "organization",
NameOID.ORGANIZATIONAL_UNIT_NAME: "organizational_unit",
NameOID.COUNTRY_NAME: "country",
NameOID.LOCALITY_NAME: "locality",
NameOID.STATE_OR_PROVINCE_NAME: "state_or_province",
NameOID.EMAIL_ADDRESS: "email_address",
NameOID.SERIAL_NUMBER: "serial_number",
NameOID.SURNAME: "surname",
NameOID.GIVEN_NAME: "given_name",
NameOID.TITLE: "title",
NameOID.JURISDICTION_COUNTRY_NAME: "jurisdiction_country",
NameOID.JURISDICTION_STATE_OR_PROVINCE_NAME: "jurisdiction_state",
NameOID.JURISDICTION_LOCALITY_NAME: "jurisdiction_locality",
NameOID.BUSINESS_CATEGORY: "business_category",
NameOID.POSTAL_CODE: "postal_code",
NameOID.STREET_ADDRESS: "street_address",
NameOID.DOMAIN_COMPONENT: "domain_component",
NameOID.USER_ID: "user_id",
# Add more as needed
}
return mapping.get(oid, oid.dotted_string)
def name_to_dict(name: x509.Name) -> dict:
return {oid_to_name(attr.oid): attr.value for attr in name}
def dns_to_dict(dns: x509.DNSName) -> dict:
return dict(value=dns.value)
def extension_to_dict(ext):
if isinstance(ext.value, x509.SubjectAlternativeName):
return {
"critical": ext.critical,
"general_names": [dns_to_dict(gn) for gn in ext.value],
}
elif isinstance(ext.value, x509.BasicConstraints):
return {
"critical": ext.critical,
"ca": ext.value.ca,
"path_length": ext.value.path_length,
}
elif isinstance(ext.value, x509.KeyUsage):
return {
"critical": ext.critical,
**{
field.lower(): getattr(ext.value, field)
for field in [
"digital_signature",
"content_commitment",
"key_encipherment",
"data_encipherment",
"key_agreement",
"key_cert_sign",
"crl_sign",
# TODO: deal with error: "ValueError: encipher_only is undefined unless key_agreement is true"
# "encipher_only",
# "decipher_only",
]
if getattr(ext.value, field) is not None
},
}
elif isinstance(ext.value, x509.ExtendedKeyUsage):
return {
"critical": ext.critical,
"usages": [eku.dotted_string for eku in ext.value],
}
elif isinstance(ext.value, x509.CRLDistributionPoints):
return {
"critical": ext.critical,
"distribution_points": [
{
"full_name": [str(uri) for uri in dp.full_name]
if dp.full_name
else None,
"crl_issuer": [str(issuer) for issuer in dp.crl_issuer]
if dp.crl_issuer
else None,
"reasons": [r.name for r in dp.reasons] if dp.reasons else None,
}
for dp in ext.value
],
}
elif isinstance(ext.value, x509.AuthorityKeyIdentifier):
return {
"critical": ext.critical,
"key_identifier": ext.value.key_identifier.hex()
if ext.value.key_identifier
else None,
"authority_cert_issuer": [
str(n) for n in ext.value.authority_cert_issuer
]
if ext.value.authority_cert_issuer
else None,
"authority_cert_serial_number": ext.value.authority_cert_serial_number,
}
elif isinstance(ext.value, x509.SubjectKeyIdentifier):
return {"critical": ext.critical, "digest": ext.value.digest.hex()}
else:
return {
"critical": ext.critical,
"oid": ext.oid.dotted_string,
"value": str(ext.value),
}
# Build the main dict
result = dict(
version=cert.version.name,
serial_number=cert.serial_number,
signature_algorithm=cert.signature_algorithm_oid._name,
issuer=name_to_dict(cert.issuer),
subject=name_to_dict(cert.subject),
validity={
"not_valid_before": cert.not_valid_before.isoformat(),
"not_valid_after": cert.not_valid_after.isoformat(),
},
public_key={
"key_size": cert.public_key().key_size,
},
extensions={
ext.oid._name
if hasattr(ext.oid, "_name") and ext.oid._name
else ext.oid.dotted_string: extension_to_dict(ext)
for ext in cert.extensions
},
fingerprint={
"sha1": cert.fingerprint(hashes.SHA1()).hex(),
"sha256": cert.fingerprint(hashes.SHA256()).hex(),
},
)
return result
def define_nock(context: Context, definitions: list[dict]):
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/bdd-nock/define",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json=dict(definitions=definitions),
)
response.raise_for_status()
def restore_nock(context: Context):
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/bdd-nock/restore",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json=dict(),
)
response.raise_for_status()
def clean_all_nock(context: Context):
jwt_token = context.vars["AUTH_TOKEN"]
response = context.http_client.post(
"/api/v1/bdd-nock/clean-all",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json=dict(),
)
response.raise_for_status()
@contextlib.contextmanager
def with_nocks(context: Context, definitions: list[dict]):
try:
define_nock(context, definitions)
yield
finally:
clean_all_nock(context)
restore_nock(context)

View File

@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICBDCCAYmgAwIBAgIIHZvNVJSPdsYwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
bWluaWNhIHJvb3QgY2EgN2ZlMDQwMB4XDTI1MTExMzAwMzAxMloXDTI3MTIxMzAw
MzAxMlowFDESMBAGA1UEAxMJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EEACID
YgAE2V5oM5JimqDjzEfH10cKu6L8eQ9rxzkULbIJRFFuuXtKQQwkcAW8L4UuMkmG
lu5hFCBR8saHDpISuAyYLYqsddxwndxmGT3zyw6oU+8oXWX0tThL0KgajmZckOfR
ysYpo4GbMIGYMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBSIDfQe2L6+9aYyBFbd
t0S51xW3UDA4BgNVHREEMTAvgglsb2NhbGhvc3SCBnBlYmJsZYIUaG9zdC5kb2Nr
ZXIuaW50ZXJuYWyHBH8AAAEwCgYIKoZIzj0EAwMDaQAwZgIxAPkeGVzCDKuJYd/1
87+lXXtlMHrW7F+Rn1kyR8SBud2hDt5r3a+ZZ8IQ9aHazRia/AIxAOI4I41jwxf0
86i7fKx8of4s/CBc4+PF0hbCBkmen3aKuiZ7ueYuEsSNT6zHV2xc2w==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBx7d0VqxwTYcJajFgz
ja0PExBmxdZjEQRfGCMQY8GfHa0WpBUEwVtBD6XOGE5xZB2hZANiAATZXmgzkmKa
oOPMR8fXRwq7ovx5D2vHORQtsglEUW65e0pBDCRwBbwvhS4ySYaW7mEUIFHyxocO
khK4DJgtiqx13HCd3GYZPfPLDqhT7yhdZfS1OEvQqBqOZlyQ59HKxik=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,28 @@
{
"pebble": {
"listenAddress": "0.0.0.0:14000",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "/var/data/pebble/localhost/cert.pem",
"privateKey": "/var/data/pebble/localhost/key.pem",
"httpPort": 5002,
"tlsPort": 5001,
"ocspResponderURL": "",
"externalAccountBindingRequired": false,
"domainBlocklist": ["blocked-domain.example"],
"retryAfter": {
"authz": 3,
"order": 5
},
"keyAlgorithm": "ecdsa",
"profiles": {
"default": {
"description": "The profile you know and love",
"validityPeriod": 7776000
},
"shortlived": {
"description": "A short-lived cert profile, without actual enforcement",
"validityPeriod": 518400
}
}
}
}

View File

@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDnPx90G0J4ba0CMTrh
AT0kJkRGyhv5ePWyobdT75za/I9MpU/VsC8BG5uJBraxiSOhZANiAAQWEiTINq0t
j+6Qiyzin74FU4/zLNuEs1FnipFn+Vb1W8qhvbBwLOGsANpaHIg4dpR+CghfccRQ
0kQm/AMgj08VXvta6vV7aQ8yk+/Cp6l4SVQ9GzizHiJ//Qb71vrXbco=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB+zCCAYKgAwIBAgIIf+BA3XMRozcwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
bWluaWNhIHJvb3QgY2EgN2ZlMDQwMCAXDTI1MTExMzAwMzAxMloYDzIxMjUxMTEz
MDAzMDEyWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA3ZmUwNDAwdjAQBgcq
hkjOPQIBBgUrgQQAIgNiAAQWEiTINq0tj+6Qiyzin74FU4/zLNuEs1FnipFn+Vb1
W8qhvbBwLOGsANpaHIg4dpR+CghfccRQ0kQm/AMgj08VXvta6vV7aQ8yk+/Cp6l4
SVQ9GzizHiJ//Qb71vrXbcqjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQW
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
DgQWBBSIDfQe2L6+9aYyBFbdt0S51xW3UDAfBgNVHSMEGDAWgBSIDfQe2L6+9aYy
BFbdt0S51xW3UDAKBggqhkjOPQQDAwNnADBkAjAK2OUUVHs2LVqwyLEqIrXbc3gw
5r5p9TC9asqPN8vJxlTRStrXnJQRSQ2KoWztiSICMEV5jZGVk6TaUwlqcGmXEmGr
iFeQ3rXLaRw8XKMqj7+EiwaCD1o2wLgzny/21NFtxQ==
-----END CERTIFICATE-----

View File

@@ -98,6 +98,7 @@
"ms": "^2.1.3",
"mysql2": "^3.9.8",
"nanoid": "^3.3.8",
"nock": "^14.0.10",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.9",
"oci-sdk": "^2.108.0",
@@ -9705,6 +9706,23 @@
"win32"
]
},
"node_modules/@mswjs/interceptors": {
"version": "0.39.8",
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz",
"integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==",
"license": "MIT",
"dependencies": {
"@open-draft/deferred-promise": "^2.2.0",
"@open-draft/logger": "^0.3.0",
"@open-draft/until": "^2.0.0",
"is-node-process": "^1.2.0",
"outvariant": "^1.4.3",
"strict-event-emitter": "^0.5.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@next/env": {
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz",
@@ -10714,6 +10732,28 @@
"urijs": "^1.19.11"
}
},
"node_modules/@open-draft/deferred-promise": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
"license": "MIT"
},
"node_modules/@open-draft/logger": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
"integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
"license": "MIT",
"dependencies": {
"is-node-process": "^1.2.0",
"outvariant": "^1.4.0"
}
},
"node_modules/@open-draft/until": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
"license": "MIT"
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -22958,6 +22998,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-node-process": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
"integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
"license": "MIT"
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -23507,6 +23553,12 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -25074,6 +25126,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/nock": {
"version": "14.0.10",
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz",
"integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==",
"license": "MIT",
"dependencies": {
"@mswjs/interceptors": "^0.39.5",
"json-stringify-safe": "^5.0.1",
"propagate": "^2.0.0"
},
"engines": {
"node": ">=18.20.0 <20 || >=20.12.1"
}
},
"node_modules/node-abi": {
"version": "3.65.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz",
@@ -27702,6 +27768,12 @@
"@otplib/preset-v11": "^12.0.1"
}
},
"node_modules/outvariant": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
"integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
"license": "MIT"
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@@ -29103,6 +29175,15 @@
"node": ">= 6"
}
},
"node_modules/propagate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/proto3-json-serializer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
@@ -31601,6 +31682,12 @@
"node": ">=4.0.0"
}
},
"node_modules/strict-event-emitter": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
"integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",

View File

@@ -226,6 +226,7 @@
"ms": "^2.1.3",
"mysql2": "^3.9.8",
"nanoid": "^3.3.8",
"nock": "^14.0.10",
"node-forge": "^1.3.1",
"nodemailer": "^6.9.9",
"oci-sdk": "^2.108.0",

View File

@@ -435,14 +435,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
message: z
.string()
.trim()
.min(1)
.max(255)
.refine((message) => message.trim() !== "", {
message: "Commit message cannot be empty"
}),
message: z.string().trim().max(255).optional(),
changes: z.object({
secrets: z.object({
create: z
@@ -546,7 +539,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: req.body.projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
message: req.body.message,
message: req.body.message || "",
changes: {
secrets: req.body.changes.secrets,
folders: req.body.changes.folders
@@ -564,7 +557,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
projectId: req.body.projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
message: req.body.message
message: req.body.message || ""
}
}
});

View File

@@ -2,7 +2,6 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors";
import {
AcmeOrderResourceSchema,
CreateAcmeAccountResponseSchema,
@@ -257,12 +256,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({
const { profileId, accountId } = await validateExistingAccount({
req
});
if (payload !== "") {
throw new AcmeMalformedError({ message: "Payload should be empty" });
}
return sendAcmeResponse(
res,
profileId,
@@ -369,12 +365,9 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({
const { profileId, accountId } = await validateExistingAccount({
req
});
if (payload !== "") {
throw new AcmeMalformedError({ message: "Payload should be empty" });
}
res.type("application/pem-certificate-chain");
return sendAcmeResponse(
res,
@@ -405,10 +398,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const { profileId, accountId, payload } = await validateExistingAccount({ req });
if (payload !== "") {
throw new AcmeMalformedError({ message: "Payload should be empty" });
}
const { profileId, accountId } = await validateExistingAccount({ req });
return sendAcmeResponse(
res,
profileId,

View File

@@ -76,7 +76,9 @@ export const pkiAcmeChallengeServiceFactory = ({
// challenge validation at the same time, it should be fine.
const challengeResponse = await fetch(challengeUrl, { signal: AbortSignal.timeout(timeoutMs) });
if (challengeResponse.status !== 200) {
throw new BadRequestError({ message: "ACME challenge response is not 200" });
throw new AcmeIncorrectResponseError({
message: `ACME challenge response is not 200: ${challengeResponse.status}`
});
}
const challengeResponseBody = await challengeResponse.text();
const thumbprint = challenge.auth.account.publicKeyThumbprint;
@@ -107,6 +109,7 @@ export const pkiAcmeChallengeServiceFactory = ({
if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
}
logger.error(exp, "Unknown error validating ACME challenge response");
return new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
}
} else if (exp instanceof DOMException) {

View File

@@ -468,7 +468,7 @@ export class AcmeOrderNotReadyError extends AcmeError {
super({
type: AcmeErrorType.OrderNotReady,
message,
status: 403,
status: 400,
error
});
this.name = "AcmeOrderNotReadyError";

View File

@@ -17,8 +17,21 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isPrivateIp } from "@app/lib/ip/ipRange";
import { logger } from "@app/lib/logger";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { ActorType } from "@app/services/auth/auth-type";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
import {
CertExtendedKeyUsage,
CertKeyUsage,
CertSubjectAlternativeNameType
} from "@app/services/certificate/certificate-types";
import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import {
EnrollmentType,
@@ -79,9 +92,14 @@ import {
} from "./pki-acme-types";
type TPkiAcmeServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
acmeAccountDAL: Pick<
TPkiAcmeAccountDALFactory,
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
@@ -102,7 +120,10 @@ type TPkiAcmeServiceFactoryDep = {
"create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation"
>;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
kmsService: Pick<
TKmsServiceFactory,
"decryptWithKmsKey" | "generateKmsKey" | "encryptWithKmsKey" | "createCipherPairWithDataKey"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
acmeChallengeService: TPkiAcmeChallengeServiceFactory;
@@ -110,8 +131,13 @@ type TPkiAcmeServiceFactoryDep = {
export const pkiAcmeServiceFactory = ({
projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL,
certificateBodyDAL,
certificateSecretDAL,
acmeAccountDAL,
acmeOrderDAL,
acmeAuthDAL,
@@ -622,6 +648,7 @@ export const pkiAcmeServiceFactory = ({
orderId: string;
payload: TFinalizeAcmeOrderPayload;
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
const profile = (await certificateProfileDAL.findByIdWithConfigs(profileId))!;
let order = await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId);
if (!order) {
throw new NotFoundError({ message: "ACME order not found" });
@@ -637,29 +664,100 @@ export const pkiAcmeServiceFactory = ({
if (finalizingOrder.expiresAt < new Date()) {
throw new AcmeOrderNotReadyError({ message: "ACME order has expired" });
}
const { csr } = payload;
// Check and validate the CSR
const certificateRequest = extractCertificateRequestFromCSR(csr);
if (!certificateRequest.commonName) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name is required" });
}
if (
certificateRequest.subjectAlternativeNames?.some(
(san) => san.type !== CertSubjectAlternativeNameType.DNS_NAME
)
) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Only DNS subject alternative names are supported" });
}
const orderWithAuthorizations = (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(
accountId,
orderId,
tx
))!;
const csrIdentifierValues = new Set(
(certificateRequest.subjectAlternativeNames ?? [])
.map((san) => san.value.toLowerCase())
.concat([certificateRequest.commonName.toLowerCase()])
);
if (
csrIdentifierValues.size !== orderWithAuthorizations.authorizations.length ||
!orderWithAuthorizations.authorizations.every((auth) =>
csrIdentifierValues.has(auth.identifierValue.toLowerCase())
)
) {
throw new AcmeBadCSRError({ message: "Invalid CSR: Common name + SANs mismatch with order identifiers" });
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
let errorToReturn: Error | undefined;
try {
const { certificateId } = await certificateV3Service.signCertificateFromProfile({
actor: ActorType.ACME_ACCOUNT,
actorId: accountId,
actorAuthMethod: null,
actorOrgId,
profileId,
csr,
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
validity: !finalizingOrder.notAfter
? {
// 47 days, the default TTL comes with Let's Encrypt
// TODO: read config from the profile to get the expiration time instead
ttl: `${47}d`
}
: // ttl is not used if notAfter is provided
({ ttl: "0d" } as const),
enrollmentType: EnrollmentType.ACME
});
// TODO: associate the certificate with the order
const { certificateId } = await (async () => {
if (caType === CaType.INTERNAL) {
const result = await certificateV3Service.signCertificateFromProfile({
actor: ActorType.ACME_ACCOUNT,
actorId: accountId,
actorAuthMethod: null,
actorOrgId,
profileId,
csr,
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
validity: !finalizingOrder.notAfter
? {
// 47 days, the default TTL comes with Let's Encrypt
// TODO: read config from the profile to get the expiration time instead
ttl: `${47}d`
}
: // ttl is not used if notAfter is provided
({ ttl: "0d" } as const),
enrollmentType: EnrollmentType.ACME
});
return { certificateId: result.certificateId };
}
const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!;
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const csrPem = csrObj.toString("pem");
// TODO: for internal CA, we rely on the internal certificate authority service to check CSR against the template
// we should check the CSR against the template here
// TODO: this is pretty slow, and we are holding the transaction open for a long time,
// we should queue the certificate issuance to a background job instead
const cert = await orderCertificate(
{
caId: certificateAuthority!.id,
commonName: certificateRequest.commonName!,
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
csr: Buffer.from(csrPem),
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT, CertKeyUsage.KEY_AGREEMENT],
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH]
},
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
);
return { certificateId: cert.id };
})();
await acmeOrderDAL.updateById(
orderId,
{

View File

@@ -106,7 +106,9 @@ const envSchema = z
HTTPS_ENABLED: zodStrBool,
ROTATION_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
DAILY_RESOURCE_CLEAN_UP_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
BDD_NOCK_API_ENABLED: zodStrBool.default("false").optional(),
ACME_DEVELOPMENT_MODE: zodStrBool.default("false").optional(),
ACME_SKIP_UPSTREAM_VALIDATION: zodStrBool.default("false").optional(),
ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES: zpStr(
z
.string()
@@ -398,6 +400,7 @@ const envSchema = z
isAcmeDevelopmentMode: data.NODE_ENV === "development" && data.ACME_DEVELOPMENT_MODE,
isProductionMode: data.NODE_ENV === "production" || IS_PACKAGED,
isRedisSentinelMode: Boolean(data.REDIS_SENTINEL_HOSTS),
isBddNockApiEnabled: data.NODE_ENV === "development" && data.BDD_NOCK_API_ENABLED,
REDIS_SENTINEL_HOSTS: data.REDIS_SENTINEL_HOSTS?.trim()
?.split(",")
.map((el) => {

View File

@@ -643,7 +643,8 @@ export const registerRoutes = async (
projectDAL,
identityDAL,
userDAL,
externalGroupOrgRoleMappingDAL
externalGroupOrgRoleMappingDAL,
membershipRoleDAL
});
const additionalPrivilegeService = additionalPrivilegeServiceFactory({
additionalPrivilegeDAL,
@@ -2244,8 +2245,13 @@ export const registerRoutes = async (
});
const pkiAcmeService = pkiAcmeServiceFactory({
projectDAL,
appConnectionDAL,
certificateDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateProfileDAL,
certificateBodyDAL,
certificateSecretDAL,
acmeAccountDAL,
acmeOrderDAL,
acmeAuthDAL,

View File

@@ -0,0 +1,88 @@
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { logger } from "@app/lib/logger";
import nock, { Definition } from "nock";
export const registerBddNockRouter = async (server: FastifyZodProvider) => {
const checkIfBddNockApiEnabled = () => {
const appCfg = getConfig();
// Note: Please note that this API is only available in development mode and only for BDD tests.
// This endpoint should NEVER BE ENABLED IN PRODUCTION!
if (appCfg.NODE_ENV !== "development" || !appCfg.isBddNockApiEnabled) {
throw new ForbiddenRequestError({ message: "BDD Nock API is not enabled" });
}
};
server.route({
method: "POST",
url: "/define",
schema: {
body: z.object({ definitions: z.unknown().array() }),
response: {
200: z.object({ status: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
checkIfBddNockApiEnabled();
const { body } = req;
const { definitions } = body;
logger.info(definitions, "Defining nock");
const processedDefinitions = definitions.map((definition: unknown) => {
const { path, ...rest } = definition as Definition;
return {
...rest,
path:
path !== undefined && typeof path === "string"
? path
: new RegExp((path as unknown as { regex: string }).regex ?? "")
} as Definition;
});
nock.define(processedDefinitions);
// Ensure we are activating the nocks, because we could have called `nock.restore()` before this call.
if (!nock.isActive()) {
nock.activate();
}
return { status: "ok" };
}
});
server.route({
method: "POST",
url: "/clean-all",
schema: {
response: {
200: z.object({ status: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => {
checkIfBddNockApiEnabled();
logger.info("Cleaning all nocks");
nock.cleanAll();
return { status: "ok" };
}
});
server.route({
method: "POST",
url: "/restore",
schema: {
response: {
200: z.object({ status: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async () => {
checkIfBddNockApiEnabled();
logger.info("Restore network requests from nock");
nock.restore();
return { status: "ok" };
}
});
};

View File

@@ -6,8 +6,10 @@ import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers";
import { getConfig } from "@app/lib/config/env";
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
import { registerBddNockRouter } from "./bdd-nock-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
@@ -237,4 +239,10 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
// Note: This is a special route for BDD tests. It's only available in development mode and only for BDD tests.
// This route should NEVER BE ENABLED IN PRODUCTION!
if (getConfig().isBddNockApiEnabled) {
await server.register(registerBddNockRouter, { prefix: "/bdd-nock" });
}
};

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { AccessScope, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { AccessScope, OrgMembershipRole, ProjectMembershipRole, ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, PROJECT_USERS } from "@app/lib/api-docs";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -51,6 +51,19 @@ export const registerDeprecatedProjectMembershipRouter = async (server: FastifyZ
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const usernamesAndEmails = [...req.body.emails, ...req.body.usernames];
await server.services.membershipUser.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
roles: [{ isTemporary: false, role: OrgMembershipRole.NoAccess }],
usernames: usernamesAndEmails
}
});
const { memberships } = await server.services.membershipUser.createMembership({
permission: req.permission,
scopeData: {

View File

@@ -1,5 +1,5 @@
import * as x509 from "@peculiar/x509";
import acme from "acme-client";
import acme, { CsrBuffer } from "acme-client";
import { TableName } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography";
@@ -29,6 +29,8 @@ import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-ut
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { getConfig } from "@app/lib/config/env";
import { Knex } from "knex";
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
import { CaStatus, CaType } from "../certificate-authority-enums";
import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns";
@@ -64,6 +66,20 @@ type TAcmeCertificateAuthorityFnsDeps = {
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};
type TOrderCertificateDeps = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
kmsService: Pick<
TKmsServiceFactory,
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
};
type DBConfigurationColumn = {
dnsProvider: string;
directoryUrl: string;
@@ -104,6 +120,245 @@ export const castDbEntryToAcmeCertificateAuthority = (
};
};
export const orderCertificate = async (
{
caId,
subscriberId,
commonName,
altNames,
csr,
csrPrivateKey,
keyUsages,
extendedKeyUsages
}: {
caId: string;
subscriberId?: string;
commonName: string;
altNames?: string[];
csr: CsrBuffer;
csrPrivateKey?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
},
deps: TOrderCertificateDeps,
tx?: Knex
) => {
const {
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
} = deps;
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId, tx);
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
throw new BadRequestError({ message: "CA is not an ACME CA" });
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
let accountKey: Buffer | undefined;
if (acmeCa.credentials) {
const decryptedCredentials = await kmsDecryptor({
cipherTextBlob: acmeCa.credentials as Buffer
});
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
JSON.parse(decryptedCredentials.toString("utf8"))
);
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
}
if (!accountKey) {
accountKey = await acme.crypto.createPrivateRsaKey();
const newCredentials = {
accountKey: accountKey.toString("base64")
};
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
plainText: Buffer.from(JSON.stringify(newCredentials))
});
await externalCertificateAuthorityDAL.update(
{
caId: acmeCa.id
},
{
credentials: encryptedNewCredentials
}
);
}
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
const acmeClientOptions: acme.ClientOptions = {
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
};
if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
acmeClientOptions.externalAccountBinding = {
kid: acmeCa.configuration.eabKid,
hmacKey: acmeCa.configuration.eabHmacKey
};
}
const acmeClient = new acme.Client(acmeClientOptions);
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
const connection = await decryptAppConnection(appConnection, kmsService);
const pem = await acmeClient.auto({
csr,
email: acmeCa.configuration.accountEmail,
challengePriority: ["dns-01"],
// For ACME development mode, we mock the DNS challenge API calls. So, no real DNS records are created.
// We need to disable the challenge verification to avoid errors.
skipChallengeVerification: getConfig().isAcmeDevelopmentMode && getConfig().ACME_SKIP_UPSTREAM_VALIDATION,
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== "dns-01") {
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareInsertTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareDeleteTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
}
});
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
const certObj = new x509.X509Certificate(leafCert);
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const certificateChainPem = parentCert.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const { cipherTextBlob: encryptedPrivateKey } = csrPrivateKey
? await kmsEncryptor({
plainText: Buffer.from(csrPrivateKey)
})
: { cipherTextBlob: undefined };
return (tx || certificateDAL).transaction(async (innerTx: Knex) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
pkiSubscriberId: subscriberId,
status: CertStatus.ACTIVE,
friendlyName: commonName,
commonName,
altNames: altNames?.join(","),
serialNumber: certObj.serialNumber,
notBefore: certObj.notBefore,
notAfter: certObj.notAfter,
keyUsages,
extendedKeyUsages,
projectId: ca.projectId
},
innerTx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
innerTx
);
if (encryptedPrivateKey !== undefined) {
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
innerTx
);
}
return cert;
});
};
export const AcmeCertificateAuthorityFns = ({
appConnectionDAL,
appConnectionService,
@@ -322,77 +577,6 @@ export const AcmeCertificateAuthorityFns = ({
if (!subscriber.caId) {
throw new BadRequestError({ message: "Subscriber does not have a CA" });
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId);
if (!ca.externalCa || ca.externalCa.type !== CaType.ACME) {
throw new BadRequestError({ message: "CA is not an ACME CA" });
}
const acmeCa = castDbEntryToAcmeCertificateAuthority(ca);
if (acmeCa.status !== CaStatus.ACTIVE) {
throw new BadRequestError({ message: "CA is disabled" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
let accountKey: Buffer | undefined;
if (acmeCa.credentials) {
const decryptedCredentials = await kmsDecryptor({
cipherTextBlob: acmeCa.credentials as Buffer
});
const parsedCredentials = await AcmeCertificateAuthorityCredentialsSchema.parseAsync(
JSON.parse(decryptedCredentials.toString("utf8"))
);
accountKey = Buffer.from(parsedCredentials.accountKey, "base64");
}
if (!accountKey) {
accountKey = await acme.crypto.createPrivateRsaKey();
const newCredentials = {
accountKey: accountKey.toString("base64")
};
const { cipherTextBlob: encryptedNewCredentials } = await kmsEncryptor({
plainText: Buffer.from(JSON.stringify(newCredentials))
});
await externalCertificateAuthorityDAL.update(
{
caId: acmeCa.id
},
{
credentials: encryptedNewCredentials
}
);
}
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
const acmeClientOptions: acme.ClientOptions = {
directoryUrl: acmeCa.configuration.directoryUrl,
accountKey
};
if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
acmeClientOptions.externalAccountBinding = {
kid: acmeCa.configuration.eabKid,
hmacKey: acmeCa.configuration.eabHmacKey
};
}
const acmeClient = new acme.Client(acmeClientOptions);
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
@@ -407,131 +591,28 @@ export const AcmeCertificateAuthorityFns = ({
skLeaf
);
const appConnection = await appConnectionDAL.findById(acmeCa.configuration.dnsAppConnectionId);
const connection = await decryptAppConnection(appConnection, kmsService);
const pem = await acmeClient.auto({
csr: certificateCsr,
email: acmeCa.configuration.accountEmail,
challengePriority: ["dns-01"],
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
if (challenge.type !== "dns-01") {
throw new Error("Unsupported challenge type");
}
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53InsertTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareInsertTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
await orderCertificate(
{
caId: subscriber.caId,
subscriberId: subscriber.id,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames,
csr: certificateCsr,
csrPrivateKey: skLeaf,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
const recordValue = `"${keyAuthorization}"`; // must be double quoted
switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: {
await route53DeleteTxtRecord(
connection as TAwsConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
case AcmeDnsProvider.Cloudflare: {
await cloudflareDeleteTxtRecord(
connection as TCloudflareConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
}
}
{
appConnectionDAL,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL
}
});
const [leafCert, parentCert] = acme.crypto.splitPemChain(pem);
const certObj = new x509.X509Certificate(leafCert);
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(certObj.rawData))
});
const certificateChainPem = parentCert.trim();
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
plainText: Buffer.from(certificateChainPem)
});
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
plainText: Buffer.from(skLeaf)
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
pkiSubscriberId: subscriber.id,
status: CertStatus.ACTIVE,
friendlyName: subscriber.commonName,
commonName: subscriber.commonName,
altNames: subscriber.subjectAlternativeNames.join(","),
serialNumber: certObj.serialNumber,
notBefore: certObj.notBefore,
notAfter: certObj.notAfter,
keyUsages: subscriber.keyUsages as CertKeyUsage[],
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[],
projectId: ca.projectId
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate,
encryptedCertificateChain
},
tx
);
await certificateSecretDAL.create(
{
certId: cert.id,
encryptedPrivateKey
},
tx
);
});
);
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
};

View File

@@ -168,15 +168,12 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
} as TCertificateProfileWithConfigs["acmeConfig"])
: undefined;
const certificateAuthority =
result.caId && result.caProjectId && result.caStatus && result.caName
? ({
id: result.caId,
projectId: result.caProjectId,
status: result.caStatus,
name: result.caName
} as TCertificateProfileWithConfigs["certificateAuthority"])
: undefined;
const certificateAuthority = {
id: result.caId,
projectId: result.caProjectId,
status: result.caStatus,
name: result.caName
} as TCertificateProfileWithConfigs["certificateAuthority"];
const certificateTemplate =
result.templateId && result.templateProjectId && result.templateName

View File

@@ -6,6 +6,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema, unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
import { ActorType } from "../auth/auth-type";
import { TExternalGroupOrgRoleMappingDALFactory } from "../external-group-org-role-mapping/external-group-org-role-mapping-dal";
@@ -33,6 +34,7 @@ type TRoleServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectDAL: Pick<TProjectDALFactory, "findById">;
externalGroupOrgRoleMappingDAL: Pick<TExternalGroupOrgRoleMappingDALFactory, "findOne">;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "find">;
};
export type TRoleServiceFactory = ReturnType<typeof roleServiceFactory>;
@@ -43,7 +45,8 @@ export const roleServiceFactory = ({
projectDAL,
identityDAL,
userDAL,
externalGroupOrgRoleMappingDAL
externalGroupOrgRoleMappingDAL,
membershipRoleDAL
}: TRoleServiceFactoryDep) => {
const orgRoleFactory = newOrgRoleFactory({
permissionService,
@@ -137,6 +140,23 @@ export const roleServiceFactory = ({
});
if (!existingRole) throw new NotFoundError({ message: `Role with ${dto.selector.id} not found` });
const [roleUsageData] = await membershipRoleDAL.find(
{
customRoleId: dto.selector.id
},
{ count: true }
);
if (roleUsageData) {
const count = Number.parseInt(roleUsageData.count, 10);
if (count > 0) {
const plural = count > 1 ? "s" : "";
throw new BadRequestError({
message: `Role is assigned to ${count} identity membership${plural}. Re-assign membership role${plural} to delete this role.`
});
}
}
const [role] = await roleDAL.delete({
id: existingRole.id,
[scope.key]: scope.value

View File

@@ -55,8 +55,12 @@ services:
- NODE_ENV=development
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
- TELEMETRY_ENABLED=false
# This is needed to trust the Pebble CA certificate, which is used for the BDD tests
- NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/pebble.minica.crt
volumes:
- ./backend/src:/app/src
# This is needed to trust the Pebble CA certificate, which is used for the BDD tests
- ./backend/bdd/pebble/pebble.minica.pem:/usr/local/share/ca-certificates/pebble.minica.crt:ro
- softhsm_tokens:/etc/softhsm2/tokens # SoftHSM tokens are stored in a volume to persist across container restarts
extra_hosts:
- "host.docker.internal:host-gateway"
@@ -75,6 +79,21 @@ services:
- ./frontend/public:/app/public
env_file: .env
# ACME server for BDD tests
pebble:
image: ghcr.io/letsencrypt/pebble:2.8.0
command: -config /var/data/pebble/pebble-config.json
ports:
- 14000:14000 # ACME port
- 15000:15000 # Management port
environment:
# Do not perform validation sleep to make the BDD tests faster
- PEBBLE_VA_NOSLEEP=1
# Skip validation for now to make the BDD tests easier to write
- PEBBLE_VA_ALWAYS_VALID=1
volumes:
- ./backend/bdd/pebble/:/var/data/pebble:ro
volumes:
postgres-data:
driver: local

View File

@@ -1,18 +0,0 @@
---
title: "Usage"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com) or your self-hosted instance.
- Obtain an API Key in your user settings to be included in requests to the Infisical API.
Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview).
## Concepts
- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by the user's password before being sent off to the server during the account signup process.
- Each (encrypted) secret belongs to a project and environment.
- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key.
- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing.
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.

View File

@@ -1,93 +0,0 @@
---
title: "FAQ"
description: "Frequently Asked Questions about contributing to Infisical"
---
Frequently asked questions about contributing to Infisical can be found on this page.
If you can't find the answer you are looking for, please create an issue on our GitHub repository or join our Slack channel for additional support.
<Accordion title="Error building Infisical platform backend (Alpine Linux CDN temporary error)">
The Alpine Linux CDN may be unavailable/down in your region infrequently (eg. there is an unplanned outage). One possible fix is to add a retry mechanism and a fallback mirrors array to the Dockerfile. You can also use this as an opportunity to pin the Alpine Linux version for Docker to use in case there are issues with the latest version. Ensure to use https for the mirrors.
#### Make the following changes to the backend Dockerfile
```bash
# Pin Alpine version from list: https://dl-cdn.alpinelinux.org/alpine/
ARG ALPINE_VERSION=3.17
ARG ALPINE_APPEND=v3.17/main
# Specify number of retries for each mirror
ARG MAX_RETRIES=3
# Define base Alpine mirror URLs in attempt order from list: https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt
ARG BASE_ALPINE_MIRRORS="https://dl-cdn.alpinelinux.org/alpine https://ftp.halifax.rwth-aachen.de/alpine https://uk.alpinelinux.org/alpine"
# Build stage
# Add the Alpine version arg
FROM node:16-alpine$ALPINE_VERSION AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only-production
COPY . .
RUN npm run build
# Production stage
# Add the Alpine version arg
FROM node:16-alpine$ALPINE_VERSION
WORKDIR /app
ENV npm_config_cache /home/node/.npm
COPY package*.json ./
RUN npm ci --only-production
COPY --from=build /app .
# Add retry mechanism and loop through the specified mirrors
RUN retries_left=$MAX_RETRIES; \
for mirror in $ALPINE_MIRRORS; do \
full_mirror="$mirror/$ALPINE_APPEND"; \
echo "Trying mirror: $full_mirror"; \
echo >>/etc/apk/repositories "$full_mirror"; \
for i in $(seq $retries_left); do \
echo "Retrying... Attempt $i (Retries Left: $((retries_left - i)))"; \
if apk add --no-cache bash curl git && \
curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash && \
apk add --no-cache infisical=0.8.1; then \
break; \
fi; \
sleep 10; \
done; \
if [ $? -eq 0 ]; then \
break; \
fi; \
done
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 4000
CMD ["npm", "run", "start"]
```
<Info>
[Alpine Linux (mirrors) - official site](https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt)
</Info>
<Info>
[Alpine Linux (mirrors) - archived site](https://web.archive.org/web/20230914123159/https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt)
</Info>
<Info>
[Alpine Linux (versions) - official site](https://dl-cdn.alpinelinux.org/alpine/)
</Info>
<Info>
[Alpine Linux (versions) - archived site](https://web.archive.org/web/20230914123455/https://dl-cdn.alpinelinux.org/alpine/)
</Info>
</Accordion>

View File

@@ -20,7 +20,6 @@ Infisical has two major code-bases. One for the platform code, and one for SDKs.
- [C++ SDK](https://github.com/Infisical/infisical-cpp-sdk)
- [PHP SDK](https://github.com/Infisical/php-sdk)
- [Rust SDK](https://github.com/Infisical/rust-sdk)
- [Ruby SDK](https://github.com/infisical/sdk)
## Community

View File

@@ -29,13 +29,7 @@ Feel free to add a short video or screenshots of what your PR achieves.
## Getting your PR reviewed
Once your PR is reviewed, one or two relevant members of the Infisical team should review and approve the PR before it is merged. You should coordinate and ping the team member closest to the submitted functionality via our [Slack](https://infisical.com/slack) to review your PR.
- Vlad: Frontend, Web UI
- Tony: Backend, SDKs, Security
- Maidul: Backend, CI/CD, CLI, Kubernetes Operator
- Daniel: Frontend, UI/UX, Backend, SDKs
One or two relevant members of the Infisical team should review and approve the PR before it is merged. You can ping someone from the team in our [Slack](https://infisical.com/slack) to review your PR.
The team member(s) will start by enabling baseline checks to ensure that there are no leaked secrets, new dependencies are clear, and the frontend/backend services start up. Afterward, they will review your PR thoroughly by testing the code and leave any feedback or work in with you to revise the PR up to standard.

View File

@@ -5,28 +5,47 @@ title: 'Backend folder structure'
```
├── scripts
├── e2e-test
├── bdd
└── src/
├── @types/
│ ├── knex.d.ts
│ └── fastify.d.ts
│ ├── fastify.d.ts
│ ├── ...
├── db/
│ ├── migrations
│ ├── schemas
│ └── seed
│ └── seeds
├── keystore/
├── lib/
│ ├── api-docs
│ ├── aws
│ ├── axios
│ ├── base64
│ ├── casl
│ ├── certificates
│ ├── config
│ ├── crypto
│ ├── dates
│ ├── delay
│ ├── error-codes
│ ├── errors
│ ├── files
│ ├── fn
│ ├── date
│ └── config
│ ├── ...
├── queue
├── server/
│ ├── routes/
│ │ ├── v1
│ │ └── v2
│ │ ├── v2
│ │ ├── v3
│ │ └── v4
│ ├── plugins
│ └── config
│ ├── config
│ └── lib
├── services/
│ ├── auth
│ ├── org
│ ├── ...
│ └── project/
│ ├── project-service.ts
│ ├── project-types.ts
@@ -42,19 +61,23 @@ Contains reusable scripts for backend automation, like running migrations and ge
### `backend/e2e-test`
Integration tests for the APIs.
### `backend/bdd`
Behavior-Driven Development (BDD) tests using Python and Gherkin feature files.
### `backend/src`
The source code of the backend.
- `@types`: Type definitions for libraries like Fastify and Knex.
- `db`: Knex.js configuration for the database, including migration, seed files, and SQL type schemas.
- `lib`: Stateless, reusable functions used across the codebase.
- `@types`: Type definitions for libraries like Fastify, Knex, and other third-party dependencies.
- `db`: Knex.js configuration for the database, including migrations, seed files, and SQL type schemas.
- `keystore`: Key-value store abstraction layer supporting Redis and PostgreSQL for application caching, distributed locking, and coordination.
- `lib`: Stateless, reusable functions used across the codebase, organized by functionality (crypto, config, dates, etc.).
- `queue`: Infisical's queue system based on BullMQ.
### `src/server`
- Scope anything related to Fastify/service here.
- Includes routes, Fastify plugins, and server configurations.
- The routes folder contains various versions of routes separated into v1, v2, etc.
- Includes routes, Fastify plugins, server configurations, and server-specific utilities.
- The routes folder contains various versions of routes separated into v1, v2, v3, v4, etc.
### `src/services`

View File

@@ -8,7 +8,7 @@ Suppose you're interested in implementing a new feature in Infisical's backend,
If your feature involves a change in the database, you need to first address this by generating the necessary database schemas.
1. If you're adding a new table, update the `TableName` enum in `/src/db/schemas/models.ts` to include the new table name.
2. Create a new migration file by running `npm run migration:new` and give it a relevant name, such as `feature-x`.
2. Create a new migration file by going to the `/backend` folder and running `npm run migration:new` and give it a relevant name, such as `feature-x`.
3. Navigate to `/src/db/migrations/<timestamp>_<feature-x>.ts`.
4. Modify both the `up` and `down` functions to create or alter Postgres fields on migration up and to revert these changes on migration down, ensuring idempotency as outlined [here](https://github.com/graphile/migrate/blob/main/docs/idempotent-examples.md).
@@ -16,10 +16,11 @@ If your feature involves a change in the database, you need to first address thi
While typically you would need to manually write TS types for Knex type-sense, we have automated this process:
1. Start the server.
2. Run `npm run migration:latest` to apply all database changes.
3. Execute `npm run generate:schema` to automatically generate types and schemas using [zod](https://github.com/colinhacks/zod) in the `/src/db/schemas` folder.
4. Update the barrel export in `schema/index` and include the new tables in `/src/@types/knex.d.ts` to enable type-sensing in Knex.js.
1. If you haven't done it yet, create a new `.env.migration` file at the root of the Infisical directory then copy the contents of the file linked [here](https://github.com/Infisical/infisical/blob/main/.env.migration.example)
2. Start the server.
3. Go to the `/backend` folder and run `npm run migration:latest-dev` to apply all database changes.
4. Execute `npm run generate:schema` to automatically generate types and schemas using [zod](https://github.com/colinhacks/zod) in the `/src/db/schemas` folder.
5. Update the barrel export in `schema/index` and include the new tables in `/src/@types/knex.d.ts` to enable type-sensing in Knex.js.
## Business Logic
@@ -38,10 +39,10 @@ Use the custom Infisical function `ormify` in `src/lib/knex` for simple database
## Connecting the Service Layer to the Server Layer
Server-related logic is handled in `/src/server`. To connect the service layer to the server layer, we use Fastify plugins for dependency injection:
Server-related logic is handled in `/src/server`. To connect the service layer to the server layer, we use Fastify's dependency injection pattern:
1. Add the service type in the `fastify.d.ts` file under the `service` namespace of a FastifyServerInstance type.
2. In `/src/server/routes/index.ts`, instantiate the required dependencies for `feature-x`, such as the DAL and service layers, and then pass them to `fastify.register("service,{...dependencies})`.
1. Add the service type in `/src/@types/fastify.d.ts` under the `services` namespace of the `FastifyInstance` interface.
2. In `/src/server/routes/index.ts`, instantiate the required dependencies for `feature-x` (such as the DAL and service layers), and then add the service instance to the `server.decorate()` call, where all services are registered for dependency injection.
3. This makes the service layer accessible within all routes under the Fastify service instance, accessed via `server.services.<registered service name>.<function>`.
## Writing API Routes

View File

@@ -15,7 +15,7 @@ git checkout -b MY_BRANCH_NAME
## Set up environment variables
Start by creating a .env file at the root of the Infisical directory then copy the contents of the file linked [here](https://github.com/Infisical/infisical/blob/main/.env.example). View all available [environment variables](https://infisical.com/docs/self-hosting/configuration/envars) and guidance for each.
Start by creating a `.env` file at the root of the Infisical directory then copy the contents of the file linked [here](https://github.com/Infisical/infisical/blob/main/.env.example). View all available [environment variables](https://infisical.com/docs/self-hosting/configuration/envars) and guidance for each.
## Starting Infisical for development

View File

@@ -377,8 +377,7 @@
"pages": [
"contributing/getting-started/overview",
"contributing/getting-started/code-of-conduct",
"contributing/getting-started/pull-requests",
"contributing/getting-started/faq"
"contributing/getting-started/pull-requests"
]
},
{
@@ -753,7 +752,12 @@
"group": "Infrastructure Integrations",
"pages": [
"documentation/platform/pki/pki-issuer",
"documentation/platform/pki/integration-guides/gloo-mesh"
"documentation/platform/pki/integration-guides/gloo-mesh",
"documentation/platform/pki/integration-guides/windows-server-acme",
"documentation/platform/pki/integration-guides/nginx-certbot",
"documentation/platform/pki/integration-guides/apache-certbot",
"documentation/platform/pki/integration-guides/tomcat-certbot",
"documentation/platform/pki/integration-guides/jboss-certbot"
]
},
{
@@ -2822,7 +2826,7 @@
"href": "https://infisical.com"
},
"api": {
"openapi": "http://localhost:8080/api/docs/json",
"openapi": "https://app.infisical.com/api/docs/json",
"mdx": {
"server": ["https://app.infisical.com"]
}

View File

@@ -1,128 +0,0 @@
---
title: "REST API"
---
Infisical's REST API is the most flexible way to read/write secrets for your application.
In this brief, we'll explore how to fetch a secret back from a project on [Infisical Cloud](https://app.infisical.com) via the REST API.
<Steps>
<Step title="Create a project with a secret">
To create a project, head to your Organization Overview and press **Add New Project**; we'll call the project **Demo App**.
![create project](../../images/getting-started/api/org-create-project-1.png)
![create project](../../images/getting-started/api/org-create-project-2.png)
Next, let's head to the **Development** environment of the project and add a secret `FOO=BAR` to it.
![explore project env](../../images/getting-started/api/project-explore-env.png)
![create secret](../../images/getting-started/api/project-create-secret.png)
![project dashboard](../../images/getting-started/api/project-dashboard.png)
<Note>
For this brief, you'll need to disable end-to-end encryption in your Project Settings
</Note>
</Step>
<Step title="Create an identity">
Next, we need to create an identity to represent your application. To create one, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](../../images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](../../images/platform/identities/identities-org-create.png)
Once you've created an identity, you'll be prompted to configure the **Universal Auth** authentication method for it.
![identities organization create auth method](../../images/platform/identities/identities-org-create-auth-method.png)
</Step>
<Step title="Create a Client Secret">
In order to use the identity, you'll need the non-sensitive **Client ID**
of the identity and a **Client Secret** for it; you can think of these credentials akin to a username
and password used to authenticate with the Infisical API. With that, press on the key icon on the identity to generate a **Client Secret**
for it.
![identities client secret create](../../images/platform/identities/identities-org-client-secret.png)
![identities client secret create](../../images/platform/identities/identities-org-client-secret-create-1.png)
![identities client secret create](../../images/platform/identities/identities-org-client-secret-create-2.png)
</Step>
<Step title="Add the identity to the project">
To enable the identity to access your project, we need to add it to the project. To do this, head over to the **Demo App** Project Settings > Access Control > Machine Identities and press **Add identity**.
Next, select the identity you want to add to the project and the role you want to assign it.
![identities project](../../images/platform/identities/identities-project.png)
![identities project create](../../images/platform/identities/identities-project-create.png)
</Step>
<Step title="Get an access token for the Infisical API">
To access the Infisical API as the identity, you should first perform a login operation
that is to exchange the **Client ID** and **Client Secret** of the identity for an access token
by making a request to the `/api/v1/auth/universal-auth/login` endpoint.
#### Sample request
```
curl --location --request POST 'https://app.infisical.com/api/v1/auth/universal-auth/login' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'clientSecret=<client_secret>' \
--data-urlencode 'clientId=<client_id>'
```
#### Sample response
```
{
"accessToken": "...",
"expiresIn": 7200,
"tokenType": "Bearer"
}
```
Next, we can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction) to read/write secrets
<Note>
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained from the aforementioned login operation.
</Note>
</Step>
<Step title="Fetch back secret">
Finally, you can fetch the secret `FOO=BAR` back from **Step 1** by including the access token in the previous step in another request to the `/api/v3/secrets/raw/{secretName}` endpoint.
### Sample request
```
curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw/FOO?workspaceId=657830d579cfc8415d06ce5b&environment=dev' \
--header 'Authorization: Bearer <access_token>'
```
### Sample response
```
{
"secret": {
"_id": "6564234b934d634e1fcd6cdf",
"version": 1,
"workspace": "6564173e934d634e1fcd6950",
"type": "shared",
"environment": "dev",
"secretKey": "FOO2",
"secretValue": "BAR2",
"secretComment": ""
}
}
```
Note that you can fetch a list of secrets back by making a request to the `/api/v3/secrets/raw` endpoint.
</Step>
</Steps>
See also:
- [API Reference](/api-reference/overview/introduction)

View File

@@ -46,12 +46,11 @@ description: "The open source platform for managing secrets, certificates, and s
>
Manage access to resources like databases, servers, and accounts with policy-based controls and approvals.
</Card>
</Columns>
<Columns cols="1">
<Card title="Infisical KMS" href="/documentation/platform/kms/overview">
Encrypt and decrypt sensitive data using a centralized key management
system.
<Card
title="Infisical KMS"
href="/documentation/platform/kms/overview"
>
Encrypt and decrypt sensitive data using a centralized key management system.
</Card>
</Columns>

View File

@@ -1,65 +0,0 @@
---
title: "Platform"
---
This quickstart provides an overview of functionalities offered by Infisical.
## Managing your Organization
When you first make an account with Infisical, you also create a new **organization** where you are assigned the `admin` role by default.
From there, you can invite external members to the organization and start creating **projects** to house secrets.
### Projects
The **Projects** page shows you all the projects that you have access to within your organization.
Here, you can also create a new project.
![organization overview](../../images/organization-overview.png)
### Members
The **Members** page lets you add or remove external members to your organization.
Note that you can configure your organization in Infisical to have members authenticate with the platform via protocols like SAML 2.0 and OpenID Connect.
![organization members](../../images/organization/platform/organization-members.png)
## Managing your Projects
As mentioned before, projects house secrets which are further organized into environments such as development, testing and production.
A project can be anything from a single application to a collection of micro-services that you wish to manage secrets for.
### Secrets Overview
The **Secrets Overview** screen provides a bird's-eye view of all the secrets in a project and is useful for comparing secrets and identifying missing ones across environments.
![dashboard secrets overview](../../images/dashboard-secrets-overview.png)
In the above image, you can already see that:
- `STRIPE_API_KEY` is missing from the **Staging** environment.
- `JWT_SECRET` is missing from the **Production** environment.
- `BAR` is `EMPTY` in the **Production** environment.
### Dashboard
The secrets dashboard lets you manage secrets for a specific environment in a project.
Here, developers can override secrets, version secrets, rollback projects to any point in time and much more.
![dashboard](../../images/dashboard.png)
### Integrations
The integrations page provides native integrations to sync secrets from a project environment to a [host of ever-expanding integrations](/integrations/overview).
![integrations](../../images/integrations.png)
### Members
The members page lets you add/remove members to/from a project and provision them access to environments via roles. By default, Infisical provides the `admin`, `developer`, and `viewer` roles
which you can assign to members.
![project members](../../images/project-members.png)
That's it for the platform quickstart! — We encourage you to continue exploring the documentation to gain a deeper understanding of the extensive features and functionalities that Infisical has to offer.
Next, head back to [Getting Started > Introduction](/documentation/getting-started/overview) to explore ways to fetch secrets from Infisical to your apps and infrastructure.

View File

@@ -199,7 +199,7 @@ Next, navigate to your project's integrations tab in Infisical and press on the
Opting in for the Infisical-Vercel integration will break end-to-end encryption since Infisical will be able to read
your secrets. This is, however, necessary for Infisical to sync the secrets to Vercel.
Your secrets remain encrypted at rest following our [security guide mechanics](/security/mechanics).
Your secrets remain encrypted at rest following our [security guide mechanics](/internals/security).
</Note>
Now select **Production** for (the source) **Environment** and sync it to the **Production Environment** of the (target) application in Vercel.
@@ -238,7 +238,7 @@ At this stage, you know how to use the Infisical-Vercel integration to sync prod
<Accordion title="Is opting out of end-to-end encryption for the Infisical-Vercel integration safe?">
Yes. Your secrets are still encrypted at rest. To note, most secret managers actually don't support end-to-end encryption.
Check out the [security guide](/security/overview).
Check out the [security guide](/internals/security).
</Accordion>
</AccordionGroup>

View File

@@ -75,7 +75,7 @@ Infisical’s access control framework is unified for both human users and machi
### 7.3 Attribute-Based Access Control (ABAC)
[Attribute-based Access Controls](/documentation/platform/access-controls/attribute-based-access-controls) allow restrictions based on tags or attributes linked to secrets. These can be integrated with SAML assertions and other security frameworks for dynamic access management.
[Attribute-based Access Controls](/documentation/platform/access-controls/abac/overview) allow restrictions based on tags or attributes linked to secrets. These can be integrated with SAML assertions and other security frameworks for dynamic access management.
### 7.4 User Groups

View File

@@ -31,7 +31,7 @@ In the following steps, we explore how to create and use user groups to provisio
![groups org](/images/platform/groups/groups-org.png)
When creating a group, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating a group, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![groups org create](/images/platform/groups/groups-org-create.png)

View File

@@ -88,7 +88,7 @@ To create an identity, head to your Organization Settings > Access Control > [Id
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
When creating an identity, you specify an organization-level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -66,7 +66,7 @@ access the Infisical API using the AWS Auth authentication method.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -66,7 +66,7 @@ access the Infisical API using the Azure Auth authentication method.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -72,7 +72,7 @@ access the Infisical API using the GCP ID Token authentication method.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)
@@ -241,7 +241,7 @@ access the Infisical API using the GCP IAM authentication method.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -61,7 +61,7 @@ In the following steps, we explore how to create and use identities to access th
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -218,7 +218,7 @@ In the following steps, we explore how to create and use identities for your app
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -102,7 +102,7 @@ To create an identity, head to your Organization Settings > Access Control > [Id
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
When creating an identity, you specify an organization-level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -59,7 +59,7 @@ In the following steps, we explore how to create and use identities to access th
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -56,7 +56,7 @@ In the following steps, we explore how to create and use identities to access th
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -60,7 +60,7 @@ In the following steps, we explore how to create and use identities to access th
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -59,7 +59,7 @@ In the following steps, we explore how to create and use identities to access th
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -59,7 +59,7 @@ In the following steps, we explore how to create and use identities to access th
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -94,7 +94,7 @@ In the following steps, we explore how to create and use identities to access th
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -68,7 +68,7 @@ To create an identity, head to your Organization Settings > Access Control > [Id
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
When creating an identity, you specify an organization-level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -42,7 +42,7 @@ using the Token Auth authentication method.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -47,7 +47,7 @@ using the Universal Auth authentication method.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
When creating an identity, you specify an organization level [role](/documentation/platform/access-controls/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](/images/platform/identities/identities-org-create.png)

View File

@@ -7,7 +7,7 @@ description: "Read more about the concept of user identities in Infisical."
A **user identity** (also known as **user**) represents a developer, admin, or any other human entity interacting with resources in Infisical.
Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects).
Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../project).
Upon being added to an organization and projects, users assume a certain set of roles and permissions that represents their identity.

View File

@@ -10,7 +10,7 @@ Infisical can be used as a Key Management System (KMS), referred to as Infisical
By default your Infisical data such as projects and the data within them are encrypted at rest using Infisical's own KMS. This ensures that your data is secure and protected from unauthorized access.
If you are on-premise, your KMS root key will be created at random with the `ROOT_ENCRYPTION_KEY` environment variable. You can also use a Hardware Security Module (HSM), to create the root key. Read more about [HSM](/docs/documentation/platform/kms/encryption-strategies).
If you are on-premise, your KMS root key will be created at random with the `ROOT_ENCRYPTION_KEY` environment variable. You can also use a Hardware Security Module (HSM), to create the root key. Read more about [HSM](/documentation/platform/kms/hsm-integration).
<Note>
Keys managed in KMS are not extractable from the platform. Additionally, data
@@ -109,7 +109,7 @@ In the following steps, we explore how to generate a key and use it to encrypt d
</Step>
<Step title="Encrypting data with the KMS key">
To encrypt data, make an API request to the [Encrypt
Data](/api-reference/endpoints/kms/keys/encrypt) API endpoint,
Data](/api-reference/endpoints/kms/encryption/encrypt) API endpoint,
specifying the key to use.
<Note>
@@ -168,7 +168,7 @@ In the following steps, we explore how to use decrypt data using an existing key
<Steps>
<Step title="Decrypting data">
To decrypt data, make an API request to the [Decrypt
Data](/api-reference/endpoints/kms/keys/decrypt) API endpoint,
Data](/api-reference/endpoints/kms/encryption/decrypt) API endpoint,
specifying the key to use.
### Sample request

View File

@@ -255,7 +255,7 @@ In the following steps, we explore how to set up ACME Certificate Authority inte
The issued certificate and private key are now available through Infisical and can be:
- Downloaded directly from the Infisical UI
- Retrieved via the Infisical API for programmatic access using the [latest certificate bundle endpoint](/api-reference/endpoints/pki/subscribers/get-latest-cert-bundle)
- Retrieved via the Infisical API for programmatic access using the [latest certificate bundle endpoint](/api-reference/endpoints/certificate-profiles/get-latest-active-bundle)
</Step>
</Steps>

View File

@@ -109,49 +109,126 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year.
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA.
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates-v2/create) API endpoint, specifying the issuing CA.
### Sample request
```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificate-templates' \
curl --request POST \
--url https://us.infisical.com/api/v2/certificate-templates \
--header 'Content-Type: application/json' \
--data-raw '{
"caId": "<ca-id>",
"name": "My Certificate Template",
"commonName": ".*.acme.com",
"subjectAlternativeName": ".*.acme.com",
"ttl": "1y",
}'
--data '{
"projectId": "<string>",
"name": "<string>",
"description": "<string>",
"subject": [
{
"type": "common_name",
"allowed": [
"*.infisical.com"
]
}
],
"sans": [
{
"type": "dns_name",
"allowed": [
"*.sample.com"
]
}
],
"keyUsages": {
"allowed": [
"digital_signature"
]
},
"extendedKeyUsages": {
"allowed": [
"client_auth"
]
},
"algorithms": {
"signature": [
"SHA256-RSA"
],
"keyAlgorithm": [
"RSA-2048"
]
},
"validity": {
"max": "365d"
}
}'
```
### Sample response
```bash Response
{
id: "...",
caId: "...",
name: "...",
commonName: "...",
subjectAlternativeName: "...",
ttl: "...",
"certificateTemplate": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"description": "<string>",
"subject": [
{
"type": "common_name",
"allowed": [
"*.infisical.com"
]
}
],
"sans": [
{
"type": "dns_name",
"allowed": [
"*.sample.com"
]
}
],
"keyUsages": {
"allowed": [
"digital_signature"
]
},
"extendedKeyUsages": {
"allowed": [
"client_auth"
]
},
"algorithms": {
"signature": [
"SHA256-RSA"
],
"keyAlgorithm": [
"RSA-2048"
]
},
"validity": {
"max": "365d"
},
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z"
}
}
```
</Step>
<Step title="Creating a certificate">
To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-certificate) API endpoint,
specifying the issuing CA.
### Sample request
```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/pki/certificates/issue-certificate' \
curl --location --request POST 'https://app.infisical.com/api/v3/pki/certificates/issue-certificate' \
--header 'Content-Type: application/json' \
--data-raw '{
"certificateTemplateId": "<certificate-template-id>",
"profileId": "<profile-id>",
"commonName": "service.acme.com",
"ttl": "1y",
"signatureAlgorithm": "RSA-SHA256",
"keyAlgorithm": "RSA_2048"
}'
```
@@ -221,16 +298,16 @@ In the following steps, we explore how to revoke a X.509 certificate under a CA
selecting the **Revoke Certificate** option for it and specifying the reason
for revocation.
![pki revoke certificate](/images/platform/pki/cert-revoke.png)
![pki revoke certificate](/images/platform/pki/certificate/cert-revoke.png)
![pki revoke certificate modal](/images/platform/pki/cert-revoke-modal.png)
![pki revoke certificate modal](/images/platform/pki/certificate/cert-revoke-modal.png)
</Step>
<Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it
against the CRL of a CA by heading to its Issuing CA and downloading the CRL.
![pki view crl](/images/platform/pki/ca-crl.png)
![pki view crl](/images/platform/pki/ca/ca-crl.png)
To verify a certificate against the
downloaded CRL with OpenSSL, you can use the following command:
@@ -254,7 +331,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem
<Tab title="API">
<Steps>
<Step title="Revoking a certificate">
Assuming that you've issued a certificate under a CA, you can revoke it by making an API request to the [Revoke Certificate](/api-reference/endpoints/certificate-authorities/revoke) API endpoint,
Assuming that you've issued a certificate under a CA, you can revoke it by making an API request to the [Revoke Certificate](/api-reference/endpoints/certificates/revoke) API endpoint,
specifying the serial number of the certificate and the reason for revocation.
### Sample request
@@ -280,7 +357,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem
</Step>
<Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA.
To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crls) API endpoint.
To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crl) API endpoint.
### Sample request

View File

@@ -206,7 +206,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem
<Tab title="API">
<Steps>
<Step title="Revoking a certificate">
Assuming that you've issued a certificate under a CA, you can revoke it by making an API request to the [Revoke Certificate](/api-reference/endpoints/certificate-authorities/revoke) API endpoint,
Assuming that you've issued a certificate under a CA, you can revoke it by making an API request to the [Revoke Certificate](/api-reference/endpoints/certificates/revoke) API endpoint,
specifying the serial number of the certificate and the reason for revocation.
### Sample request
@@ -232,7 +232,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem
</Step>
<Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA.
To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crls) API endpoint.
To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crl) API endpoint.
### Sample request

View File

@@ -12,4 +12,4 @@ There are three components to understand:
- [Certificate Template](/documentation/platform/pki/certificates/templates): A policy structure specifying the permitted attributes for requested certificates including subject naming conventions, SAN fields, key usages, and extended key usages.
- [Certificate](/documentation/platform/pki/certificates/certificate): The actual X.509 certificate issued for a profile. Once issued, a certificate kept track of in the certificate inventory.
- [Certificate](/documentation/platform/pki/certificates/certificates): The actual X.509 certificate issued for a profile. Once issued, a certificate kept track of in the certificate inventory.

View File

@@ -56,7 +56,7 @@ Here, select the certificate profile from step 1 that will be used to issue the
<Steps>
<Step title="Create a certificate profile">
To create a certificate [profile](/documentation/platform/pki/certificates/profiles), make an API request to the [Create Certificate Profile](/docs/api-reference/endpoints/certificate-profiles/create) API endpoint.
To create a certificate [profile](/documentation/platform/pki/certificates/profiles), make an API request to the [Create Certificate Profile](/api-reference/endpoints/certificate-profiles/create) API endpoint.
### Sample request

View File

@@ -1,59 +0,0 @@
---
title: "Enrollment over Secure Transport (EST)"
sidebarTitle: "Enrollment over Secure Transport (EST)"
description: "Learn how to manage certificate enrollment of clients using EST"
---
## Concept
Enrollment over Secure Transport (EST) is a protocol used to automate the secure provisioning of digital certificates for devices and applications over a secure HTTPS connection. It is primarily used when a client device needs to obtain or renew a certificate from a Certificate Authority (CA) on Infisical in a secure and standardized manner. EST is commonly employed in environments requiring strong authentication and encrypted communication, such as in IoT, enterprise networks, and secure web services.
Infisical's EST service is based on [RFC 7030](https://datatracker.ietf.org/doc/html/rfc7030) and implements the following endpoints:
- **cacerts** - provides the necessary CA chain for the client to validate certificates issued by the CA.
- **simpleenroll** - allows an EST client to request a new certificate from Infisical's EST server
- **simplereenroll** - similar to the /simpleenroll endpoint but is used for renewing an existing certificate.
These endpoints are exposed on port 8443 under the .well-known/est path e.g.
`https://app.infisical.com:8443/.well-known/est/estLabel/cacerts`
## Prerequisites
- You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca).
- The client devices need to have a bootstrap/pre-installed certificate.
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates.
- For Infisical Cloud users, the devices must be configured to trust the [Amazon root CA certificates](https://www.amazontrust.com/repository).
## Guide to configuring EST
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificates#guide-to-issuing-certificates).
2. Proceed to the certificate template's enrollment settings
![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png)
3. Select **EST** as the client enrollment method and fill up the remaining fields.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png)
- **Disable Bootstrap Certificate Validation** - Enable this if your devices are not configured with a bootstrap certificate.
- **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server.
- **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password.
For security reasons, Infisical authenticates EST clients using both client certificate and passphrase.
4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png)
The complete URL of the supported EST endpoints will look like the following:
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simplereenroll
## Setting up EST clients
- To use the EST passphrase in your clients, configure it as the EST password. The EST username can be set to any arbitrary value.
- Use the appropriate client certificates for invoking the EST endpoints.
- For `simpleenroll`, use the bootstrapped/manufacturer client certificate.
- For `simplereenroll`, use a valid EST-issued client certificate.
- When configuring the PKCS#12 objects for the client certificates, only include the leaf certificate and the private key.

View File

@@ -0,0 +1,185 @@
---
title: "Apache Server"
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Apache Server with Certbot"
---
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Apache HTTP Server](https://httpd.apache.org/).
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Apache benefits from excellent Certbot integration, allowing both certificate-only mode and automatic SSL configuration.
## Prerequisites
Before you begin, make sure you have:
- An [Apache HTTP Server](https://httpd.apache.org/) running on a Linux system with administrative access.
- A [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) in Infisical.
- Network connectivity from your Apache server to Infisical.
- Port 80 open and reachable for ACME [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) validation.
## Guide
<Steps>
<Step title="Obtain ACME Configuration from Infisical">
Navigate to your certificate management project in Infisical and locate your [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme).
![Certificate profile with ACME enrollment option](/images/platform/pki/acme/certificate-profile-acme-option.png)
Click the **Reveal ACME EAB** option to view the ACME configuration details.
![ACME configuration modal showing directory URL and EAB credentials](/images/platform/pki/acme/acme-configuration-modal.png)
From the ACME configuration, gather the following values:
- ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`.
- EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request.
- EAB Secret: A secret key that authenticates your ACME client with Infisical.
<Note>
Keep your EAB credentials secure as they authenticate your ACME client with Infisical PKI. These credentials are unique to each [certificate profile](/documentation/platform/pki/certificates/profiles) and should not be shared.
</Note>
</Step>
<Step title="Install Certbot">
Install Certbot with the Apache plugin on the server where Apache is running by following the official Certbot [installation guide](https://certbot.eff.org/instructions).
The installation guide provides up-to-date instructions for various Linux distributions and package managers, ensuring you get the most current version and proper Apache plugin integration.
After installation, you can verify that Certbot has been installed correctly by running:
```bash
certbot --version
```
</Step>
<Step title="Request Certificate Using Certbot">
Run the following command to request a certificate from Infisical:
```bash
sudo certbot certonly \
--apache \
--server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \
--eab-kid "your-eab-key-identifier" \
--eab-hmac-key "your-eab-secret" \
-d example.infisical.com \
--email admin@example.com \
--agree-tos \
--non-interactive
```
For guidance on each parameter:
- `certonly`: Instructs Certbot to request a certificate without modifying your Apache configuration files; this mode is recommended if you prefer to manage your Apache SSL configuration manually or have a complex setup.
- `--apache`: Specifies the Apache plugin so Certbot can solve the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge by creating temporary files served by Apache.
- `--server`: The Infisical ACME directory URL from Step 1. This instructs Certbot to communicate with Infisical's ACME server instead of Let's Encrypt.
- `--eab-kid`: Your External Account Binding (EAB) Key Identifier from Step 1.
- `--eab-hmac-key`: The EAB secret associated with the KID from Step 1.
- `-d`: Specifies the domain name for which the certificate is being requested.
- `--email`: The contact email for expiration notices and account recovery.
- `--agree-tos`: Accepts the ACME server's Terms of Service.
- `--non-interactive`: Runs Certbot without prompting for user input (recommended for automation).
The Certbot command generates a private key on your server, creates a Certificate Signing Request (CSR) using that key, and sends the CSR to Infisical for certificate issuance. Certbot stores the private key and resulting leaf certificate and full certificate chain in `/etc/letsencrypt/live/{domain-name}/`.
If `--certonly` is used: Certbot does **not** modify your Apache configuration, so you must manually update your Apache virtual host to reference the new certificate files and reload the server to apply the changes.
Here's an example SSL virtual host configuration for Apache:
```apache
<VirtualHost *:443>
ServerName example.infisical.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.infisical.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.infisical.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/example.infisical.com/chain.pem
# Your existing configuration...
</VirtualHost>
```
After updating the virtual host configuration, test and reload Apache to apply the changes:
```bash
sudo apache2ctl configtest
sudo systemctl reload apache2
```
If `--certonly` was **not** used: Certbot uses installer mode, which attempts to automatically configure HTTPS by updating your Apache virtual host configuration and reloading the server if needed.
At this point, your Apache server should be successfully serving HTTPS using the certificate issued by Infisical.
</Step>
<Step title="Verify Certificate Installation">
After configuring Apache SSL, verify that your certificate was issued correctly and Apache is serving it properly.
Check that the certificate files were created by Certbot:
```bash
sudo ls -la /etc/letsencrypt/live/example.infisical.com/
```
You should see files like:
- `cert.pem` (your certificate)
- `chain.pem` (certificate chain)
- `fullchain.pem` (certificate + chain)
- `privkey.pem` (private key)
</Step>
<Step title="Renew Your Certificate with Certbot">
Certbot automatically installs a `systemd` timer during installation. This timer runs twice per day and checks whether any certificates are due for renewal. Because Certbot stores the ACME server URL and EAB credentials from your initial request, renewal will automatically use the same Infisical ACME configuration—no additional settings are required.
Note that Certbot automatically renews certificates when they are within 30 days of expiration; renewal settings can be adjusted in `/etc/letsencrypt/renewal/{domain-name}.conf`.
```ini
# ... your existing configuration ...
renew_before_expiry = 30 days
```
To test the renewal process, run the following command:
```bash
sudo certbot renew --dry-run
```
This command simulates the full renewal process without modifying your active certificate. If the dry run succeeds, automatic renewal will work as expected.
To trigger an actual renewal immediately, run the following command:
```bash
sudo certbot renew --force-renewal
```
Note that after a certificate is renewed, Apache must be reloaded so it can begin using the new certificate. To do this, run the following command:
```bash
sudo systemctl reload apache2
```
To automate the process of renewing a certificate and reloading Apache, you can create a simple deploy hook that Certbot will run after every successful renewal.
Inside `/etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh`, add the following:
```bash
#!/bin/sh
systemctl reload apache2
```
Then make the hook executable:
```bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh
```
Alternatively, you can use the `--post-hook` option when manually renewing:
```bash
sudo certbot renew --post-hook "systemctl reload apache2"
```
<Note>
Certbot automatically renews certificates when they are within 30 days of expiration using its built-in systemd timer. The deploy hook above will run after each successful renewal, handling the Apache reload automatically. Apache has native Certbot plugin integration, so no additional configuration is typically needed.
</Note>
</Step>
</Steps>

View File

@@ -0,0 +1,226 @@
---
title: "JBoss/WildFly"
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on JBoss/WildFly with Certbot"
---
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [JBoss](https://www.jboss.org/)/[WildFly](https://wildfly.org/) application server.
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). JBoss/WildFly requires certificates in Java keystore format, which this guide addresses through the certificate conversion process.
## Prerequisites
Before you begin, make sure you have:
- A [JBoss](https://www.jboss.org/)/[WildFly](https://wildfly.org/) application server running on a Linux system with administrative access.
- A [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) in Infisical.
- Network connectivity from your JBoss/WildFly server to Infisical.
- Port 80 open and reachable for ACME [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) validation.
- [Java Development Kit (JDK)](https://openjdk.org/) installed for keystore management tools.
## Guide
<Steps>
<Step title="Obtain ACME Configuration from Infisical">
Navigate to your certificate management project in Infisical and locate your [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme).
![Certificate profile with ACME enrollment option](/images/platform/pki/acme/certificate-profile-acme-option.png)
Click the **Reveal ACME EAB** option to view the ACME configuration details.
![ACME configuration modal showing directory URL and EAB credentials](/images/platform/pki/acme/acme-configuration-modal.png)
From the ACME configuration, gather the following values:
- ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`.
- EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request.
- EAB Secret: A secret key that authenticates your ACME client with Infisical.
<Note>
Keep your EAB credentials secure as they authenticate your ACME client with Infisical PKI. These credentials are unique to each [certificate profile](/documentation/platform/pki/certificates/profiles) and should not be shared.
</Note>
</Step>
<Step title="Install Certbot">
Install Certbot on the server where JBoss/WildFly is running by following the official Certbot [installation guide](https://certbot.eff.org/instructions).
The installation guide provides up-to-date instructions for various Linux distributions and package managers, ensuring you get the most current version of Certbot.
After installation, you can verify that Certbot has been installed correctly by running:
```bash
certbot --version
```
</Step>
<Step title="Request Certificate Using Certbot">
Since JBoss/WildFly doesn't have a native Certbot plugin, use the standalone authenticator to obtain certificates. **Important**: You must stop JBoss/WildFly before running this command as Certbot needs to bind to port 80 for the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge.
Stop your JBoss/WildFly server:
```bash
sudo systemctl stop wildfly
# or for older JBoss versions
# sudo systemctl stop jboss
```
Run the following command to request a certificate from Infisical:
```bash
sudo certbot certonly \
--standalone \
--server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \
--eab-kid "your-eab-key-identifier" \
--eab-hmac-key "your-eab-secret" \
-d example.infisical.com \
--email admin@example.com \
--agree-tos \
--non-interactive
```
For guidance on each parameter:
- `certonly`: Instructs Certbot to request a certificate without modifying your JBoss/WildFly configuration; this mode is recommended because JBoss/WildFly requires certificates in Java keystore format rather than the PEM format that Certbot provides.
- `--standalone`: Uses Certbot's standalone authenticator to solve the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge by starting a temporary web server on port 80.
- `--server`: The Infisical ACME directory URL from Step 1. This instructs Certbot to communicate with Infisical's ACME server instead of Let's Encrypt.
- `--eab-kid`: Your External Account Binding (EAB) Key Identifier from Step 1.
- `--eab-hmac-key`: The EAB secret associated with the KID from Step 1.
- `-d`: Specifies the domain name for which the certificate is being requested.
- `--email`: The contact email for expiration notices and account recovery.
- `--agree-tos`: Accepts the ACME server's Terms of Service.
- `--non-interactive`: Runs Certbot without prompting for user input (recommended for automation).
The Certbot command generates a private key on your server, creates a Certificate Signing Request (CSR) using that key, and sends the CSR to Infisical for certificate issuance. Certbot stores the private key and resulting leaf certificate and full certificate chain in `/etc/letsencrypt/live/{domain-name}/`.
Because JBoss/WildFly requires certificates in Java keystore format, you'll need to convert the PEM certificates provided by Certbot in the next step.
</Step>
<Step title="Convert Certificate to Java Keystore">
JBoss/WildFly requires certificates in Java keystore format rather than the PEM format provided by Certbot. Convert the PEM certificates to PKCS#12 format, which is supported by modern JBoss/WildFly versions.
Create a PKCS#12 keystore from the PEM files:
```bash
sudo openssl pkcs12 -export \
-out /opt/wildfly/standalone/configuration/keystore.p12 \
-inkey /etc/letsencrypt/live/example.infisical.com/privkey.pem \
-in /etc/letsencrypt/live/example.infisical.com/cert.pem \
-certfile /etc/letsencrypt/live/example.infisical.com/chain.pem \
-passout pass:changeit
```
Set appropriate file permissions for security:
```bash
sudo chown wildfly:wildfly /opt/wildfly/standalone/configuration/keystore.p12
sudo chmod 600 /opt/wildfly/standalone/configuration/keystore.p12
```
You will need to configure JBoss/WildFly to use the new keystore. This process varies depending on your JBoss/WildFly version and security configuration (legacy security realms vs. Elytron subsystem). Refer to your [JBoss](https://access.redhat.com/documentation/en-us/red_hat_jboss_enterprise_application_platform)/[WildFly](https://docs.wildfly.org/) administration guide for specific SSL/TLS configuration steps.
<Note>
Replace `changeit` with a strong password and adjust the WildFly installation path based on your environment. Modern WildFly versions support PKCS#12 keystores directly, while older versions may require conversion to JKS format using the [keytool](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html) utility.
</Note>
</Step>
<Step title="Verify Certificate Installation">
After configuring JBoss/WildFly SSL, verify that your certificate was issued correctly and the keystore was created properly.
Check that the certificate files were created by Certbot:
```bash
sudo ls -la /etc/letsencrypt/live/example.infisical.com/
```
You should see files like:
- `cert.pem` (your certificate)
- `chain.pem` (certificate chain)
- `fullchain.pem` (certificate + chain)
- `privkey.pem` (private key)
Verify the PKCS#12 keystore was created:
```bash
sudo ls -la /opt/wildfly/standalone/configuration/keystore.p12
```
Test the keystore contents (optional):
```bash
sudo keytool -list -storetype PKCS12 -keystore /opt/wildfly/standalone/configuration/keystore.p12 -storepass changeit
```
Once you've configured JBoss/WildFly to use the keystore and restarted the service, you can verify HTTPS is working by accessing your application over SSL.
</Step>
<Step title="Renew Your Certificate with Certbot">
Unlike standard web servers, JBoss/WildFly certificate renewal requires additional steps because certificates must be converted to Java keystore format and the application server must be restarted to use the new certificates.
To test the renewal process without affecting your live certificates, run the following command:
```bash
sudo certbot renew --dry-run
```
This command simulates the full renewal process without modifying your active certificate. If the dry run succeeds, the renewal mechanism itself will work as expected.
For actual renewal, since JBoss/WildFly requires the standalone authenticator, you'll need to stop the server, perform the renewal, convert the certificate, and restart:
```bash
# Stop JBoss/WildFly
sudo systemctl stop wildfly
# Renew the certificate
sudo certbot renew --quiet
# Convert to keystore format
sudo openssl pkcs12 -export \
-out /opt/wildfly/standalone/configuration/keystore.p12 \
-inkey /etc/letsencrypt/live/example.infisical.com/privkey.pem \
-in /etc/letsencrypt/live/example.infisical.com/cert.pem \
-certfile /etc/letsencrypt/live/example.infisical.com/chain.pem \
-passout pass:changeit
# Set permissions
sudo chown wildfly:wildfly /opt/wildfly/standalone/configuration/keystore.p12
sudo chmod 600 /opt/wildfly/standalone/configuration/keystore.p12
# Start JBoss/WildFly
sudo systemctl start wildfly
```
To automate this process, you can create a renewal script. Create `/etc/letsencrypt/renewal-hooks/deploy/jboss-renewal.sh`:
```bash
#!/bin/bash
# JBoss/WildFly certificate renewal hook
DOMAIN="example.infisical.com"
KEYSTORE_PATH="/opt/wildfly/standalone/configuration/keystore.p12"
KEYSTORE_PASSWORD="changeit"
# Convert certificate to keystore format
openssl pkcs12 -export \
-out "$KEYSTORE_PATH" \
-inkey "/etc/letsencrypt/live/$DOMAIN/privkey.pem" \
-in "/etc/letsencrypt/live/$DOMAIN/cert.pem" \
-certfile "/etc/letsencrypt/live/$DOMAIN/chain.pem" \
-passout "pass:$KEYSTORE_PASSWORD"
# Set permissions
chown wildfly:wildfly "$KEYSTORE_PATH"
chmod 600 "$KEYSTORE_PATH"
# Restart WildFly to load new certificate
systemctl restart wildfly
```
Make the hook executable:
```bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/jboss-renewal.sh
```
<Note>
Certbot automatically renews certificates when they are within 30 days of expiration using its built-in systemd timer. The deploy hook above will run after each successful renewal, handling the keystore conversion and service restart automatically. Because JBoss/WildFly requires the standalone authenticator (which stops the service temporarily), plan for brief service interruptions during renewal.
</Note>
</Step>
</Steps>

View File

@@ -0,0 +1,175 @@
---
title: "Nginx"
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Nginx with Certbot"
---
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Nginx](https://nginx.org/) server.
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles).
## Prerequisites
Before you begin, make sure you have:
- An [Nginx](https://nginx.org/) web server running on a Linux system with administrative access.
- A [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) in Infisical.
- Network connectivity from your Nginx server to Infisical.
- Port 80 open and reachable for ACME [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) validation.
## Guide
<Steps>
<Step title="Obtain ACME Configuration from Infisical">
Navigate to your certificate management project in Infisical and locate your [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme).
![Certificate profile with ACME enrollment option](/images/platform/pki/acme/certificate-profile-acme-option.png)
Click the **Reveal ACME EAB** option to view the ACME configuration details.
![ACME configuration modal showing directory URL and EAB credentials](/images/platform/pki/acme/acme-configuration-modal.png)
From the ACME configuration, gather the following values:
- ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`.
- EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request.
- EAB Secret: A secret key that authenticates your ACME client with Infisical.
<Note>
Keep your EAB credentials secure as they authenticate your ACME client with Infisical PKI. These credentials are unique to each [certificate profile](/documentation/platform/pki/certificates/profiles) and should not be shared.
</Note>
</Step>
<Step title="Install Certbot">
Install Certbot on the server where Nginx is running by following the official Certbot [installation guide](https://certbot.eff.org/instructions).
The installation guide provides up-to-date instructions for various Linux distributions and package managers, ensuring you get the most current version and proper Nginx plugin integration.
After installation, you can verify that Certbot has been installed correctly by running:
```bash
certbot --version
```
</Step>
<Step title="Request Certificate Using Certbot">
Run the following command to request a certificate from Infisical:
```bash
sudo certbot certonly \
--nginx \
--server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \
--eab-kid "your-eab-key-identifier" \
--eab-hmac-key "your-eab-secret" \
-d example.infisical.com \
--email admin@example.com \
--agree-tos \
--non-interactive
```
For guidance on each parameter:
- `certonly`: Instructs Certbot to request a certificate without modifying and reloading your Nginx configuration file(s); this mode is recommended if you prefer to manage your Nginx TLS configuration manually, use automation tools, or integrate certificates into an existing deployment workflow.
- `--nginx`: Specifies the Nginx plugin so Certbot can solve the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge by creating temporary files served by Nginx.
- `--server`: The Infisical ACME directory URL from Step 1. This instructs Certbot to communicate with Infisical's ACME server instead of Let's Encrypt.
- `--eab-kid`: Your External Account Binding (EAB) Key Identifier from Step 1.
- `--eab-hmac-key`: The EAB secret associated with the KID from Step 1.
- `-d`: Specifies the domain name for which the certificate is being requested.
- `--email`: The contact email for expiration notices and account recovery.
- `--agree-tos`: Accepts the ACME server’s Terms of Service.
- `--non-interactive`: Runs Certbot without prompting for user input (recommended for automation).
The Certbot command generates a private key on your server, creates a Certificate Signing Request (CSR) using that key, and sends the CSR to Infisical for certificate issuance. Certbot stores the private key and resulting leaf certificate and full certificate chain in `/etc/letsencrypt/live/{domain-name}/`.
If `--certonly` is used: Certbot does **not** modify your Nginx configuration, so you must manually update your Nginx server block to reference the new certificate files and reload the server to apply the changes.
Here's how you can configure your server block:
```nginx
server {
listen 443 ssl;
server_name example.infisical.com;
ssl_certificate /etc/letsencrypt/live/example.infisical.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.infisical.com/privkey.pem;
# ...your existing configuration...
}
```
After updating the server block, you should test and reload Nginx to apply the changes:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
If `--certonly` was **not** used: Certbot uses Nginx installer mode, which attempts to automatically configure HTTPS by updating your Nginx server block and reloading the server if needed.
At this point, your Nginx server should be successfully serving HTTPS using the certificate issued by Infisical.
</Step>
<Step title="Verify Certificate Installation">
After configuring Nginx SSL, verify that your certificate was issued correctly and Nginx is serving it properly.
Check that the certificate files were created by Certbot:
```bash
sudo ls -la /etc/letsencrypt/live/example.infisical.com/
```
You should see files like:
- `cert.pem` (your certificate)
- `chain.pem` (certificate chain)
- `fullchain.pem` (certificate + chain)
- `privkey.pem` (private key)
</Step>
<Step title="Renew Your Certificate with Certbot">
Certbot automatically installs a `systemd` timer during installation. This timer runs twice per day and checks whether any certificates are due for renewal. Because Certbot stores the ACME server URL and EAB credentials from your initial request, renewal will automatically use the same Infisical ACME configuration—no additional settings are required.
Note that Certbot automatically renews certificates when they are within 30 days of expiration; renewal settings can be adjusted in `/etc/letsencrypt/renewal/{domain-name}.conf`.
```ini
# ... your existing configuration ...
renew_before_expiry = 30 days
```
To test the renewal process, run the following command:
```bash
sudo certbot renew --dry-run
```
This command simulates the full renewal process without modifying your active certificate. If the dry run succeeds, automatic renewal will work as expected.
To trigger an actual renewal immediately, run the following command:
```bash
sudo certbot renew --force-renewal
```
Note that after a certificate is renewed, Nginx must be reloaded so it can begin using the new certificate. To do this, run the following command:
```bash
systemctl reload nginx
```
To automate the process of renewing a certificate and reloading Nginx, you can create a simple deploy hook that Certbot will run after every successful renewal.
Inside `/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh`, add the following:
```bash
#!/bin/sh
systemctl reload nginx
```
Then make the hook executable:
```bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
```
</Step>
</Steps>

View File

@@ -0,0 +1,251 @@
---
title: "Tomcat"
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Tomcat with Certbot"
---
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Apache Tomcat](https://tomcat.apache.org/) application server.
It uses [Certbot](https://certbot.eff.org/), an installable [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Unlike web servers with native Certbot plugins, Tomcat requires certificates to be manually configured after issuance.
## Prerequisites
Before you begin, make sure you have:
- An [Apache Tomcat](https://tomcat.apache.org/) application server running on a Linux system with administrative access.
- A [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) in Infisical.
- Network connectivity from your Tomcat server to Infisical.
- Port 80 open and reachable for ACME [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) validation.
## Guide
<Steps>
<Step title="Obtain ACME Configuration from Infisical">
Navigate to your certificate management project in Infisical and locate your [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme).
![Certificate profile with ACME enrollment option](/images/platform/pki/acme/certificate-profile-acme-option.png)
Click the **Reveal ACME EAB** option to view the ACME configuration details.
![ACME configuration modal showing directory URL and EAB credentials](/images/platform/pki/acme/acme-configuration-modal.png)
From the ACME configuration, gather the following values:
- ACME Directory URL: The URL that Certbot will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`.
- EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request.
- EAB Secret: A secret key that authenticates your ACME client with Infisical.
<Note>
Keep your EAB credentials secure as they authenticate your ACME client with Infisical PKI. These credentials are unique to each [certificate profile](/documentation/platform/pki/certificates/profiles) and should not be shared.
</Note>
</Step>
<Step title="Install Certbot">
Install Certbot on the server where Tomcat is running by following the official Certbot [installation guide](https://certbot.eff.org/instructions).
The installation guide provides up-to-date instructions for various Linux distributions and package managers, ensuring you get the most current version of Certbot.
After installation, you can verify that Certbot has been installed correctly by running:
```bash
certbot --version
```
</Step>
<Step title="Request Certificate Using Certbot">
Since Tomcat doesn't have a native Certbot plugin, use the standalone authenticator to obtain certificates. **Important**: You must stop Tomcat before running this command as Certbot needs to bind to port 80 for the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge.
Stop your Tomcat server:
```bash
sudo systemctl stop tomcat
```
Run the following command to request a certificate from Infisical:
```bash
sudo certbot certonly \
--standalone \
--server "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" \
--eab-kid "your-eab-key-identifier" \
--eab-hmac-key "your-eab-secret" \
-d example.infisical.com \
--email admin@example.com \
--agree-tos \
--non-interactive
```
For guidance on each parameter:
- `certonly`: Instructs Certbot to request a certificate without modifying your Tomcat configuration; this mode is recommended because Tomcat requires manual SSL connector configuration in its server.xml file.
- `--standalone`: Uses Certbot's standalone authenticator to solve the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge by starting a temporary web server on port 80.
- `--server`: The Infisical ACME directory URL from Step 1. This instructs Certbot to communicate with Infisical's ACME server instead of Let's Encrypt.
- `--eab-kid`: Your External Account Binding (EAB) Key Identifier from Step 1.
- `--eab-hmac-key`: The EAB secret associated with the KID from Step 1.
- `-d`: Specifies the domain name for which the certificate is being requested.
- `--email`: The contact email for expiration notices and account recovery.
- `--agree-tos`: Accepts the ACME server's Terms of Service.
- `--non-interactive`: Runs Certbot without prompting for user input (recommended for automation).
The Certbot command generates a private key on your server, creates a Certificate Signing Request (CSR) using that key, and sends the CSR to Infisical for certificate issuance. Certbot stores the private key and resulting leaf certificate and full certificate chain in `/etc/letsencrypt/live/{domain-name}/`.
Because Tomcat requires manual SSL configuration, you'll need to configure the SSL connector in your Tomcat server.xml file to reference these certificate files. You can restart Tomcat after the certificate is issued, but SSL won't be enabled until you complete the server configuration.
```bash
sudo systemctl start tomcat
```
</Step>
<Step title="Configure Tomcat SSL Connector">
To enable SSL/TLS in Tomcat, you need to configure an SSL connector in the server.xml configuration file. Tomcat can use the PEM certificates directly without conversion to Java keystore format (available in Tomcat 8.5+ with the NIO or NIO2 connector).
Edit your Tomcat server.xml file (typically located at `/opt/tomcat/conf/server.xml` or `/usr/share/tomcat/conf/server.xml`):
```xml
<Connector port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150"
SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateFile="/etc/letsencrypt/live/example.infisical.com/cert.pem"
certificateKeyFile="/etc/letsencrypt/live/example.infisical.com/privkey.pem"
certificateChainFile="/etc/letsencrypt/live/example.infisical.com/chain.pem"
type="RSA" />
</SSLHostConfig>
</Connector>
```
Restart Tomcat to apply the SSL configuration:
```bash
sudo systemctl restart tomcat
```
You can verify SSL is working by accessing your Tomcat application at `https://example.infisical.com:8443`. For production deployments, consider configuring a reverse proxy (like [Apache HTTP Server](https://httpd.apache.org/) or [Nginx](https://nginx.org/)) to handle SSL termination on standard port 443.
<Note>
The certificate paths must be readable by the Tomcat user. You may need to adjust file permissions or copy the certificates to a location accessible by Tomcat. For security, ensure the private key file has restricted permissions (600) and is owned by the Tomcat user.
</Note>
</Step>
<Step title="Verify Certificate Installation">
After configuring Tomcat SSL, verify that your certificate was issued correctly and Tomcat is serving it properly.
Check that the certificate files were created by Certbot:
```bash
sudo ls -la /etc/letsencrypt/live/example.infisical.com/
```
You should see files like:
- `cert.pem` (your certificate)
- `chain.pem` (certificate chain)
- `fullchain.pem` (certificate + chain)
- `privkey.pem` (private key)
</Step>
<Step title="Renew Your Certificate with Certbot">
Unlike web servers with native Certbot plugins, Tomcat certificate renewal requires stopping the server, renewing the certificate, and restarting to load the new certificates.
To test the renewal process without affecting your live certificates, run the following command:
```bash
sudo certbot renew --dry-run
```
This command simulates the full renewal process without modifying your active certificate. If the dry run succeeds, the renewal mechanism will work as expected.
For actual renewal, since Tomcat requires the standalone authenticator, you'll need to stop the server, perform the renewal, and restart:
```bash
# Stop Tomcat
sudo systemctl stop tomcat
# Renew the certificate
sudo certbot renew --quiet
# Start Tomcat
sudo systemctl start tomcat
```
**Important considerations for Tomcat renewal:**
Because Tomcat uses the standalone authenticator, the server must be stopped during renewal. This creates a service interruption that requires manual coordination:
1. **Plan maintenance windows** for certificate renewals (typically every 60-90 days)
2. **Monitor renewal dates** to schedule downtime appropriately
3. **Consider load balancers** or multiple instances for high availability during renewals
Create a deploy hook to automate post-renewal tasks. Create `/etc/letsencrypt/renewal-hooks/deploy/tomcat-renewal.sh`:
```bash
#!/bin/bash
# Tomcat certificate renewal hook
# This runs AFTER Certbot successfully renews certificates
DOMAIN="example.infisical.com"
TOMCAT_USER="tomcat"
# Ensure certificate files are readable by Tomcat
chown root:$TOMCAT_USER "/etc/letsencrypt/live/$DOMAIN/cert.pem"
chown root:$TOMCAT_USER "/etc/letsencrypt/live/$DOMAIN/privkey.pem"
chown root:$TOMCAT_USER "/etc/letsencrypt/live/$DOMAIN/chain.pem"
chown root:$TOMCAT_USER "/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
# Set appropriate permissions
chmod 640 "/etc/letsencrypt/live/$DOMAIN/cert.pem"
chmod 640 "/etc/letsencrypt/live/$DOMAIN/privkey.pem"
chmod 640 "/etc/letsencrypt/live/$DOMAIN/chain.pem"
chmod 640 "/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
# Start Tomcat (it was stopped for renewal)
systemctl start tomcat
# Wait for startup and verify service is running
sleep 10
if ! systemctl is-active --quiet tomcat; then
echo "ERROR: Tomcat failed to start after certificate renewal"
exit 1
fi
echo "Tomcat certificate renewal completed successfully"
```
Make the hook executable:
```bash
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/tomcat-renewal.sh
```
<Note>
Certbot automatically renews certificates when they are within 30 days of expiration using its built-in systemd timer. The deploy hook above will run after each successful renewal, handling the certificate permissions and service restart automatically. Because Tomcat requires the standalone authenticator (which stops the service temporarily), plan for brief service interruptions during renewal.
</Note>
</Step>
<Step title="Using Renewed Certificates Manually">
If you need to manually apply renewed certificates to Tomcat (when the deploy hook isn't used), follow these steps:
**Step 1: Set certificate file permissions**
After Certbot renews your certificates, ensure they're readable by Tomcat:
```bash
sudo chown root:tomcat /etc/letsencrypt/live/example.infisical.com/cert.pem
sudo chown root:tomcat /etc/letsencrypt/live/example.infisical.com/privkey.pem
sudo chown root:tomcat /etc/letsencrypt/live/example.infisical.com/chain.pem
sudo chmod 640 /etc/letsencrypt/live/example.infisical.com/cert.pem
sudo chmod 640 /etc/letsencrypt/live/example.infisical.com/privkey.pem
sudo chmod 640 /etc/letsencrypt/live/example.infisical.com/chain.pem
```
**Step 2: Restart Tomcat to load new certificates**
```bash
sudo systemctl restart tomcat
```
That's it! Tomcat will automatically use the renewed certificates since your `server.xml` already points to the Let's Encrypt certificate files.
<Note>
Since Tomcat reads certificates from the file system on startup, you only need to restart the service after certificate renewal. The certificate file paths in `/etc/letsencrypt/live/` are symbolic links that automatically point to the latest certificates.
</Note>
</Step>
</Steps>

View File

@@ -0,0 +1,194 @@
---
title: "Windows Server"
description: "Learn how to issue SSL/TLS certificates from Infisical using ACME enrollment on Windows Server with win-acme"
---
This guide demonstrates how to use Infisical to issue SSL/TLS certificates for your [Windows Server](https://www.microsoft.com/en-us/windows-server) environments.
It uses [win-acme](https://www.win-acme.com/), a feature-rich [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment) client designed specifically for Windows, to request and renew certificates from Infisical using the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) configured on a [certificate profile](/documentation/platform/pki/certificates/profiles). Win-acme offers excellent integration with IIS, Windows Certificate Store, and various certificate storage options.
## Prerequisites
Before you begin, make sure you have:
- A [Windows Server](https://www.microsoft.com/en-us/windows-server) instance running with administrative access.
- A [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) in Infisical.
- Network connectivity from your Windows Server to Infisical.
## Guide
<Steps>
<Step title="Obtain ACME Configuration from Infisical">
Navigate to your certificate management project in Infisical and locate your [certificate profile](/documentation/platform/pki/certificates/profiles) configured with the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme).
![Certificate profile with ACME enrollment option](/images/platform/pki/acme/certificate-profile-acme-option.png)
Click the **Reveal ACME EAB** option to view the ACME configuration details.
![ACME configuration modal showing directory URL and EAB credentials](/images/platform/pki/acme/acme-configuration-modal.png)
From the ACME configuration, gather the following values:
- ACME Directory URL: The URL that win-acme will use to communicate with Infisical's ACME server. This takes the form `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`.
- EAB Key Identifier (KID): A unique identifier that tells Infisical which ACME account is making the request.
- EAB Secret: A secret key that authenticates your ACME client with Infisical.
<Note>
Keep your EAB credentials secure as they authenticate your ACME client with Infisical PKI. These credentials are unique to each [certificate profile](/documentation/platform/pki/certificates/profiles) and should not be shared.
</Note>
</Step>
<Step title="Install win-acme">
Install win-acme on your Windows Server using one of the following methods.
<Tabs>
<Tab title="Download from GitHub">
1. Visit the [win-acme releases page](https://github.com/win-acme/win-acme/releases).
2. Download the latest stable release ZIP file.
3. Extract the contents to a folder (e.g., `C:\win-acme`).
4. Open Command Prompt or PowerShell as Administrator.
5. Navigate to the win-acme folder.
```powershell
cd C:\win-acme
```
</Tab>
<Tab title=".NET Tool (Global Install)">
If you have [.NET Core](https://dotnet.microsoft.com/en-us/download) installed, you can install win-acme as a global tool:
```powershell
dotnet tool install win-acme --global
```
This makes the `wacs` command available system-wide.
</Tab>
</Tabs>
</Step>
<Step title="Request Certificate Using Command Line">
Run the following win-acme command to request a certificate from Infisical:
```powershell
wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pemfiles --pemfilespath "C:\certificates" --verbose
```
For guidance on each parameter:
- `--target manual`: Specifies manual target configuration for domain specification.
- `--host`: The domain name for which the certificate is being requested.
- `--baseuri`: The Infisical ACME directory URL from Step 1. This instructs win-acme to communicate with Infisical's ACME server instead of other ACME providers.
- `--eab-key-identifier`: Your External Account Binding (EAB) Key Identifier from Step 1.
- `--eab-key`: The EAB secret associated with the KID from Step 1.
- `--validation selfhosting`: Uses self-hosting validation method to solve the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge.
- `--store pemfiles`: Stores certificates as PEM files in a specified directory.
- `--pemfilespath`: Directory where certificates will be saved on your Windows Server.
- `--verbose`: Enables detailed logging for troubleshooting and monitoring the certificate request process.
The win-acme command generates a private key on your server, creates a Certificate Signing Request (CSR) using that key, and sends the CSR to Infisical for certificate issuance. Win-acme stores the private key and resulting leaf certificate and full certificate chain in the specified directory path.
<Note>
Replace the placeholder values with your actual configuration:
- `example.infisical.com`: Your actual domain name
- `https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory`: Your Infisical ACME endpoint from Step 1
- `your-eab-key-identifier` and `your-eab-secret`: Your External Account Binding credentials from Step 1
- `C:\certificates`: Your desired certificate storage location
</Note>
</Step>
<Step title="Alternative Storage Options">
Win-acme supports various certificate storage options beyond PEM files. Here are common alternatives for different deployment scenarios:
<Tabs>
<Tab title="Windows Certificate Store">
Store certificates directly in the [Windows Certificate Store](https://docs.microsoft.com/en-us/windows-hardware/drivers/install/certificate-stores) for integration with IIS and other Windows services:
```powershell
wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store certificatestore --verbose
```
</Tab>
<Tab title="PFX Files">
Generate [PFX files](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/certutil) with password protection for easy deployment across Windows environments:
```powershell
wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pfxfile --pfxfilepath "C:\certificates" --pfxpassword "your-secure-password" --verbose
```
</Tab>
<Tab title="IIS Central SSL">
For IIS Central SSL store integration in high-scale environments:
```powershell
wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store centralssl --centralsslstore "C:\CentralSSL" --verbose
```
</Tab>
</Tabs>
</Step>
<Step title="Configure Automatic Renewal">
Win-acme can automatically create a [Windows Scheduled Task](https://docs.microsoft.com/en-us/windows/win32/taskschd/about-the-task-scheduler) for certificate renewal. Because win-acme stores the ACME server URL and EAB credentials from your initial request, renewal will automatically use the same Infisical ACME configuration—no additional settings are required.
**Option 1: Enable during initial certificate request**
Include the `--setuptaskscheduler` parameter in your initial command to automatically create the renewal task:
```powershell
wacs.exe --target manual --host example.infisical.com --baseuri "https://your-infisical-instance.com/api/v1/pki/certificate-profiles/{profile-id}/acme/directory" --eab-key-identifier "your-eab-key-identifier" --eab-key "your-eab-secret" --validation selfhosting --store pemfiles --pemfilespath "C:\certificates" --setuptaskscheduler --verbose
```
**Option 2: Test manual renewal**
You can test the renewal process manually before setting up automation to ensure the configuration works correctly:
```powershell
wacs.exe --renew --force --verbose
```
This command simulates the full renewal process and verifies that win-acme can successfully contact Infisical and renew your certificate using the stored configuration.
**Option 3: Verify scheduled task creation**
Check that the scheduled task was created successfully:
```powershell
Get-ScheduledTask -TaskName "*win-acme*"
```
The automatic renewal task will:
- Run under the SYSTEM account for elevated privileges.
- Check certificates daily for renewal eligibility.
- Automatically renew certificates that are within the renewal threshold (typically 30 days before expiration).
- Log renewal activities to Windows Event Viewer and win-acme log files for monitoring and troubleshooting.
<Note>
Win-acme stores renewal configurations automatically in its settings directory, so once a certificate is created, the renewal process will use the same parameters (ACME endpoint, EAB credentials, storage options) for future renewals. The renewal threshold can be adjusted in the win-acme configuration files if needed.
</Note>
</Step>
<Step title="Verify Certificate Installation">
After successful certificate issuance, verify that the certificate files have been created correctly based on your chosen storage method.
<Tabs>
<Tab title="PEM Files">
Check your specified PEM files directory to ensure all certificate components are present:
```powershell
Get-ChildItem "C:\certificates" -Filter "*.pem"
```
You should see files like:
- `example.infisical.com-crt.pem` (certificate)
- `example.infisical.com-key.pem` (private key)
- `example.infisical.com-chain.pem` (complete certificate chain)
- `example.infisical.com-chain-only.pem` (only certificate chain)
![Windows Server Generated PEM files](/images/platform/pki/integrations/windows-server/certificates-created.png)
</Tab>
<Tab title="Windows Certificate Store">
If you used the certificate store option, check that the certificate was properly installed using PowerShell:
```powershell
Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -like "*example.infisical.com*"}
```
The certificate should appear in the [Local Computer Personal certificate store](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/working-with-certificates#certificate-stores), making it available for use with IIS, other Windows services, and applications that integrate with the Windows Certificate Store.
</Tab>
</Tabs>
</Step>
</Steps>

View File

@@ -106,13 +106,14 @@ In the following steps, we'll explore how to use a project template when creatin
Your project will be provisioned with the configured template roles and environments.
</Tab>
<Tab title="API">
To use a project template, make an API request to the [Create Project](/api-reference/endpoints/workspaces/create-workspace) API endpoint with the specified template name included.
To use a project template, make an API request to the [Create Project](/api-reference/endpoints/projects/create-project) API endpoint with the specified template name included.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v2/workspace \
--url https://app.infisical.com/api/v1/projects \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"projectName": "My Project",

View File

@@ -98,8 +98,8 @@ Using a __30-Day__ rotation interval as an example, here's how the process unfol
## Infisical Secret Rotation Strategies
- [PostgreSQL Credentials](./postgres)
- [Microsoft SQL Server Credentials](./mssql)
- [PostgreSQL Credentials](./postgres-credentials)
- [Microsoft SQL Server Credentials](./mssql-credentials)
## FAQ

View File

@@ -16,7 +16,7 @@ customized depending on the intended use case.
## Secrets Overview
The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments.
The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](/documentation/platform/folder) across environments.
This is useful for comparing secrets, identifying if anything is missing, and making quick changes.
![project secrets overview](/images/platform/project/project-secrets-overview-open.png)

View File

@@ -21,6 +21,6 @@ Because certificates are time-bound and centrally managed, they’re easier to a
Infisical SSH gives you a secure, scalable way to manage infrastructure access using SSH certificates — without the overhead of running your own certificate authority, wiring trust across hosts, or building issuance workflows from scratch.
It replaces long-lived SSH keys with short-lived, identity-bound certificates and handles all the moving parts for you: operating CAs, configuring trust between users and hosts, and issuing certificates on demand. With Infisical SSH, you can register a host with [`infisical ssh add-host`](/docs/cli/commands/ssh#infisical-ssh-add-host), then connect with [`infisical ssh connect`](/docs/cli/commands/ssh#infisical-ssh-connect) — that’s all it takes.
It replaces long-lived SSH keys with short-lived, identity-bound certificates and handles all the moving parts for you: operating CAs, configuring trust between users and hosts, and issuing certificates on demand. With Infisical SSH, you can register a host with [`infisical ssh add-host`](/cli/commands/ssh#infisical-ssh-add-host), then connect with [`infisical ssh connect`](/cli/commands/ssh#infisical-ssh-connect) — that’s all it takes.
The result is centralized, auditable SSH access that’s easy to use and built to scale with your infrastructure.

View File

@@ -77,7 +77,7 @@ Prerequisites:
</Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](/documentation/platform/admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>

View File

@@ -103,7 +103,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
</Steps>
<Tip>
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](/documentation/platform/admin-panel/server-admin#default-organization) to expedite OIDC login.
</Tip>
<Note>

View File

@@ -17,13 +17,13 @@ This guide will provide step by step instructions on how to configure Slack inte
![org-slack-overview](/images/platform/workflow-integrations/slack-integration/org-slack-integration-overview.png)
</Step>
<Step title="Install Slack app to workspace">
Press "Add" and select "Slack" as the platform.
Press **Add** and select **Slack** as the platform.
![org-slack-initial-add](/images/platform/workflow-integrations/slack-integration/org-slack-integration-initial-add.png)
Give your Slack integration a descriptive alias. You will use this to select the Slack integration for your project.
![org-slack-add-form](/images/platform/workflow-integrations/slack-integration/org-slack-integration-add-form.png)
Press **Connect Slack**. This opens up the Slack app installation flow. Select the Slack workspace you want to install the custom Slack app to and press **Allow**.
Press **Connect Slack**. This opens up the Slack app installation flow. Select the Slack workspace you want to install the custom Slack app to and press **Install Infisical**.
![org-slack-authenticate](/images/platform/workflow-integrations/slack-integration/cloud-org-slack-integration-authenticate.png)
This completes the workflow integration creation flow. The projects in your organization can now use this Slack integration to send real-time updates to your Slack workspace.
@@ -38,6 +38,7 @@ This guide will provide step by step instructions on how to configure Slack inte
<Steps>
<Step title="Navigate to the Workflow Integrations tab in the project settings">
Press **Add** and select **Slack** as the platform.
![project-slack-overview](/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png)
</Step>
<Step title="Select the Slack integration to use for the project">
@@ -66,13 +67,13 @@ This guide will provide step by step instructions on how to configure Slack inte
Before anything else, you need to setup the Slack app to be used by
your Infisical instance. Because you're self-hosting, you will need to
create this Slack application as demonstrated in the preceding step.
Click the **Create Slack app** button. This will open up a new window with the
custom app creation flow on Slack.
![admin-settings-slack-overview](/images/platform/workflow-integrations/slack-integration/admin-slack-integration-overview.png)
</Step>
<Step title="Create Slack app">
Click the "Create Slack app" button. This will open up a new window with the
custom app creation flow on Slack.
![admin-slack-create-app](/images/platform/workflow-integrations/slack-integration/admin-slack-integration-create-app.png)
Select the Slack workspace you want to integrate with Infisical.
![admin-slack-app-workspace-select](/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-workspace-select.png)
@@ -87,7 +88,7 @@ This guide will provide step by step instructions on how to configure Slack inte
Copy the Client ID and Client Secret values from your newly created custom Slack app and add them to Infisical.
![admin-slack-app-credentials](/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credentials.png)
![admin-slack-app-credentials-form](/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credential-form.png)
Complete the admin setup by pressing Save.
Complete the admin setup by pressing **Save**.
</Step>
</Steps>
@@ -101,13 +102,13 @@ This guide will provide step by step instructions on how to configure Slack inte
![org-slack-overview](/images/platform/workflow-integrations/slack-integration/org-slack-integration-overview.png)
</Step>
<Step title="Install Slack app to workspace">
Press "Add" and select "Slack" as the platform.
Press **Add** and select **Slack** as the platform.
![org-slack-initial-add](/images/platform/workflow-integrations/slack-integration/org-slack-integration-initial-add.png)
Give your Slack integration a descriptive alias. You will use this to select the Slack integration for your project.
![org-slack-add-form](/images/platform/workflow-integrations/slack-integration/org-slack-integration-add-form.png)
Press **Connect Slack**. This opens up the Slack app installation flow. Select the Slack workspace you want to install the custom Slack app to and press **Allow**.
Press **Connect Slack**. This opens up the Slack app installation flow. Select the Slack workspace you want to install the custom Slack app to and press **Install Infisical**.
![org-slack-authenticate](/images/platform/workflow-integrations/slack-integration/org-slack-integration-authenticate.png)
Your Slack bot will then be added to your selected Slack workspace. This completes the workflow integration creation flow. Your projects in the organization can now use this Slack integration to send real-time updates to your Slack workspace.
@@ -122,6 +123,7 @@ This guide will provide step by step instructions on how to configure Slack inte
<Steps>
<Step title="Navigate to the Workflow Integrations tab in the project settings">
Press **Add** and select **Slack** as the platform.
![project-slack-overview](/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png)
</Step>
<Step title="Select the Slack integration to use for the project">
@@ -162,3 +164,87 @@ This guide will provide step by step instructions on how to configure Slack inte
channels](/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png)
</Step>
</Steps>
## Slack Events
The Slack integration supports the following events that can be configured for your projects. Each event is triggered when specific actions occur within your Infisical instance.
<Tabs>
<Tab title="Access Request">
### Access Request
This event is triggered when a user creates a new access approval request for a project. The notification includes details about the requester, the requested permissions, the secret path and environment, and whether the access is temporary or permanent.
**When it's triggered:**
- A user submits a new access approval request through the Infisical UI
- The request requires approval based on configured access approval policies
- The notification is sent to the configured access request channels
**Notification includes:**
- Requester's full name and email
- Requested permissions (read, write, etc.)
- Secret path and environment
- Access type (temporary or permanent)
- Optional user note
- Direct link to review the request
![access request notification](/images/platform/workflow-integrations/slack-integration/access-request-notification.png)
### Access Request Updated
This event is triggered when an existing access approval request is modified or updated. This helps approvers stay informed about changes to pending requests.
**When it's triggered:**
- An access approval request is edited by the requester or another authorized user
- Changes are made to permissions, temporary range, or notes
- The notification is sent to the configured access request channels
**Notification includes:**
- Original requester's information
- Editor's full name and email (who made the update)
- Updated permissions
- Updated secret path and environment
- Editor's note explaining the changes
- Direct link to review the updated request
![access request updated notification](/images/platform/workflow-integrations/slack-integration/access-request-updated-notification.png)
</Tab>
<Tab title="Secret Approval">
### Secret Approval
This event is triggered when a secret approval request is created. This occurs when a user attempts to create, update, or delete secrets that require approval based on secret approval policies.
**When it's triggered:**
- A user creates, updates, or deletes secrets in a path protected by a secret approval policy
- The changes require approval before being applied
- The notification is sent to the configured secret request channels
**Notification includes:**
- User's email who initiated the change
- Environment and secret path
- List of secret keys affected
- Direct link to review and approve the secret changes
![secret approval notification](/images/platform/workflow-integrations/slack-integration/secret-approval-notification.png)
</Tab>
<Tab title="Secret Sync Error">
### Secret Sync Error
This event is triggered when a secret sync operation fails. Secret syncs allow you to synchronize secrets between Infisical and external systems like GitHub, GitLab, AWS Secrets Manager, and others.
**When it's triggered:**
- A secret sync fails to push secrets to the destination
- A secret sync fails to pull secrets from the source
- A secret sync fails to import secrets
- A secret sync fails to remove secrets
- Any other error occurs during the sync process
**Notification includes:**
- Sync name and destination
- The action that failed
- Environment and secret path
- Project name
- Detailed error message explaining the failure
- Direct link to view and troubleshoot the sync configuration
![secret sync error notification](/images/platform/workflow-integrations/slack-integration/secret-sync-error-notification.png)
</Tab>
</Tabs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 736 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Some files were not shown because too many files have changed in this diff Show More