diff --git a/.env.example b/.env.example index 5220d5a03..f67488c23 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.env.migration.example b/.env.migration.example index 2c5f5b957..dfc54d171 100644 --- a/.env.migration.example +++ b/.env.migration.example @@ -1,2 +1,2 @@ -DB_CONNECTION_URI= +DB_CONNECTION_URI=postgres://infisical:infisical@localhost:5432/infisical AUDIT_LOGS_DB_CONNECTION_URI= diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 16a828578..a8a64e7b4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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). 📝 - - \ No newline at end of file +- [ ] 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). 📝 \ No newline at end of file diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index bf2075864..7b54aa44e 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -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 diff --git a/README.md b/README.md index 8c7c0e82b..1a44117a4 100644 --- a/README.md +++ b/README.md @@ -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 | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index 52615036a..9a2e9f90b 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -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) diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index bee46c3fb..67f73aab2 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -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" diff --git a/backend/bdd/features/pki/acme/external-ca.feature b/backend/bdd/features/pki/acme/external-ca.feature new file mode 100644 index 000000000..26bfd84ad --- /dev/null +++ b/backend/bdd/features/pki/acme/external-ca.feature @@ -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" + ] + """ \ No newline at end of file diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index d9004e0ba..46b10c13e 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -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"(? 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 diff --git a/backend/bdd/features/steps/utils.py b/backend/bdd/features/steps/utils.py new file mode 100644 index 000000000..4ee7c8921 --- /dev/null +++ b/backend/bdd/features/steps/utils.py @@ -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"(? 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) diff --git a/backend/bdd/pebble/localhost/cert.pem b/backend/bdd/pebble/localhost/cert.pem new file mode 100644 index 000000000..9117526df --- /dev/null +++ b/backend/bdd/pebble/localhost/cert.pem @@ -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----- diff --git a/backend/bdd/pebble/localhost/key.pem b/backend/bdd/pebble/localhost/key.pem new file mode 100644 index 000000000..93b93eada --- /dev/null +++ b/backend/bdd/pebble/localhost/key.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBx7d0VqxwTYcJajFgz +ja0PExBmxdZjEQRfGCMQY8GfHa0WpBUEwVtBD6XOGE5xZB2hZANiAATZXmgzkmKa +oOPMR8fXRwq7ovx5D2vHORQtsglEUW65e0pBDCRwBbwvhS4ySYaW7mEUIFHyxocO +khK4DJgtiqx13HCd3GYZPfPLDqhT7yhdZfS1OEvQqBqOZlyQ59HKxik= +-----END PRIVATE KEY----- diff --git a/backend/bdd/pebble/pebble-config.json b/backend/bdd/pebble/pebble-config.json new file mode 100644 index 000000000..013f6ff64 --- /dev/null +++ b/backend/bdd/pebble/pebble-config.json @@ -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 + } + } + } +} \ No newline at end of file diff --git a/backend/bdd/pebble/pebble.minica.key.pem b/backend/bdd/pebble/pebble.minica.key.pem new file mode 100644 index 000000000..322b4e88e --- /dev/null +++ b/backend/bdd/pebble/pebble.minica.key.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDnPx90G0J4ba0CMTrh +AT0kJkRGyhv5ePWyobdT75za/I9MpU/VsC8BG5uJBraxiSOhZANiAAQWEiTINq0t +j+6Qiyzin74FU4/zLNuEs1FnipFn+Vb1W8qhvbBwLOGsANpaHIg4dpR+CghfccRQ +0kQm/AMgj08VXvta6vV7aQ8yk+/Cp6l4SVQ9GzizHiJ//Qb71vrXbco= +-----END PRIVATE KEY----- diff --git a/backend/bdd/pebble/pebble.minica.pem b/backend/bdd/pebble/pebble.minica.pem new file mode 100644 index 000000000..030ca32bb --- /dev/null +++ b/backend/bdd/pebble/pebble.minica.pem @@ -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----- diff --git a/backend/package-lock.json b/backend/package-lock.json index cfe22d916..8f1112cb7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 7a5efcb78..db94de681 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/ee/routes/v1/pit-router.ts b/backend/src/ee/routes/v1/pit-router.ts index 26909d294..3fa992601 100644 --- a/backend/src/ee/routes/v1/pit-router.ts +++ b/backend/src/ee/routes/v1/pit-router.ts @@ -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 || "" } } }); diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index d58790039..c4ccf6be5 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -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, diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts index 61bd0c110..9148b0336 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-service.ts @@ -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) { diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 837dec8be..9053be391 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -468,7 +468,7 @@ export class AcmeOrderNotReadyError extends AcmeError { super({ type: AcmeErrorType.OrderNotReady, message, - status: 403, + status: 400, error }); this.name = "AcmeOrderNotReadyError"; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 09f8f1772..43da08b1c 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -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; + projectDAL: Pick; + appConnectionDAL: Pick; + certificateDAL: Pick; + certificateAuthorityDAL: Pick; + externalCertificateAuthorityDAL: Pick; certificateProfileDAL: Pick; - certificateBodyDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; acmeAccountDAL: Pick< TPkiAcmeAccountDALFactory, "findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create" @@ -102,7 +120,10 @@ type TPkiAcmeServiceFactoryDep = { "create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation" >; keyStore: Pick; - kmsService: Pick; + kmsService: Pick< + TKmsServiceFactory, + "decryptWithKmsKey" | "generateKmsKey" | "encryptWithKmsKey" | "createCipherPairWithDataKey" + >; licenseService: Pick; certificateV3Service: Pick; 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> => { + 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, { diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 6f0502184..96107306f 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -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) => { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index fab7ebde3..5dd7a1c22 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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, diff --git a/backend/src/server/routes/v1/bdd-nock-router.ts b/backend/src/server/routes/v1/bdd-nock-router.ts new file mode 100644 index 000000000..ad4777772 --- /dev/null +++ b/backend/src/server/routes/v1/bdd-nock-router.ts @@ -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" }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index b480a5144..4d8b87e60 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -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" }); + } }; diff --git a/backend/src/server/routes/v2/deprecated-project-membership-router.ts b/backend/src/server/routes/v2/deprecated-project-membership-router.ts index 4dff4d5ea..aa693bcb5 100644 --- a/backend/src/server/routes/v2/deprecated-project-membership-router.ts +++ b/backend/src/server/routes/v2/deprecated-project-membership-router.ts @@ -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: { diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts index 0dea986d6..52761e6a0 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -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; }; +type TOrderCertificateDeps = { + appConnectionDAL: Pick; + certificateAuthorityDAL: Pick; + externalCertificateAuthorityDAL: Pick; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" + >; + projectDAL: Pick; +}; + 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 }); }; diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 53172b744..d2f468248 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -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 diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts index 3387dc96b..653a00b5c 100644 --- a/backend/src/services/role/role-service.ts +++ b/backend/src/services/role/role-service.ts @@ -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; projectDAL: Pick; externalGroupOrgRoleMappingDAL: Pick; + membershipRoleDAL: Pick; }; export type TRoleServiceFactory = ReturnType; @@ -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 diff --git a/docker-compose.bdd.yml b/docker-compose.bdd.yml index dfee5f6b2..b73683867 100644 --- a/docker-compose.bdd.yml +++ b/docker-compose.bdd.yml @@ -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 diff --git a/docs/api-reference/overview/usage.mdx b/docs/api-reference/overview/usage.mdx deleted file mode 100644 index 9f23080c7..000000000 --- a/docs/api-reference/overview/usage.mdx +++ /dev/null @@ -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. diff --git a/docs/contributing/getting-started/faq.mdx b/docs/contributing/getting-started/faq.mdx deleted file mode 100644 index f34382c64..000000000 --- a/docs/contributing/getting-started/faq.mdx +++ /dev/null @@ -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. - - -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"] - ``` - - - [Alpine Linux (mirrors) - official site](https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt) - - - - [Alpine Linux (mirrors) - archived site](https://web.archive.org/web/20230914123159/https://dl-cdn.alpinelinux.org/alpine/MIRRORS.txt) - - - - [Alpine Linux (versions) - official site](https://dl-cdn.alpinelinux.org/alpine/) - - - - [Alpine Linux (versions) - archived site](https://web.archive.org/web/20230914123455/https://dl-cdn.alpinelinux.org/alpine/) - - - diff --git a/docs/contributing/getting-started/overview.mdx b/docs/contributing/getting-started/overview.mdx index 1784b77e8..e34f715b0 100644 --- a/docs/contributing/getting-started/overview.mdx +++ b/docs/contributing/getting-started/overview.mdx @@ -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 diff --git a/docs/contributing/getting-started/pull-requests.mdx b/docs/contributing/getting-started/pull-requests.mdx index 2f3163478..dfdf05c18 100644 --- a/docs/contributing/getting-started/pull-requests.mdx +++ b/docs/contributing/getting-started/pull-requests.mdx @@ -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. diff --git a/docs/contributing/platform/backend/folder-structure.mdx b/docs/contributing/platform/backend/folder-structure.mdx index abfe0f69d..ec0bd72e4 100644 --- a/docs/contributing/platform/backend/folder-structure.mdx +++ b/docs/contributing/platform/backend/folder-structure.mdx @@ -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` diff --git a/docs/contributing/platform/backend/how-to-create-a-feature.mdx b/docs/contributing/platform/backend/how-to-create-a-feature.mdx index f02040cfa..52fc5aad0 100644 --- a/docs/contributing/platform/backend/how-to-create-a-feature.mdx +++ b/docs/contributing/platform/backend/how-to-create-a-feature.mdx @@ -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/_.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..`. ## Writing API Routes diff --git a/docs/contributing/platform/developing.mdx b/docs/contributing/platform/developing.mdx index 69710baf3..5a1af52c4 100644 --- a/docs/contributing/platform/developing.mdx +++ b/docs/contributing/platform/developing.mdx @@ -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 diff --git a/docs/docs.json b/docs/docs.json index 9419dd424..aea022fd4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -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"] } diff --git a/docs/documentation/getting-started/api.mdx b/docs/documentation/getting-started/api.mdx deleted file mode 100644 index c638c4d70..000000000 --- a/docs/documentation/getting-started/api.mdx +++ /dev/null @@ -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. - - - - 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) - - - For this brief, you'll need to disable end-to-end encryption in your Project Settings - - - - 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) - - - - 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) - - - 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) - - - 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=' \ - --data-urlencode 'clientId=' - ``` - - #### 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 - - - 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. - - - - 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 ' - ``` - - ### 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. - - - -See also: - -- [API Reference](/api-reference/overview/introduction) diff --git a/docs/documentation/getting-started/overview.mdx b/docs/documentation/getting-started/overview.mdx index 05ca88b5f..e0b0788ca 100644 --- a/docs/documentation/getting-started/overview.mdx +++ b/docs/documentation/getting-started/overview.mdx @@ -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. - - - - - Encrypt and decrypt sensitive data using a centralized key management - system. + + Encrypt and decrypt sensitive data using a centralized key management system. diff --git a/docs/documentation/getting-started/platform.mdx b/docs/documentation/getting-started/platform.mdx deleted file mode 100644 index 7ce96a0ed..000000000 --- a/docs/documentation/getting-started/platform.mdx +++ /dev/null @@ -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. diff --git a/docs/documentation/guides/nextjs-vercel.mdx b/docs/documentation/guides/nextjs-vercel.mdx index f4dd9ceca..ecefb0f2e 100644 --- a/docs/documentation/guides/nextjs-vercel.mdx +++ b/docs/documentation/guides/nextjs-vercel.mdx @@ -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). 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 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). diff --git a/docs/documentation/guides/organization-structure.mdx b/docs/documentation/guides/organization-structure.mdx index 752c06ccc..8693c8c1c 100644 --- a/docs/documentation/guides/organization-structure.mdx +++ b/docs/documentation/guides/organization-structure.mdx @@ -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 diff --git a/docs/documentation/platform/groups.mdx b/docs/documentation/platform/groups.mdx index 18bfe6fa5..df4f11436 100644 --- a/docs/documentation/platform/groups.mdx +++ b/docs/documentation/platform/groups.mdx @@ -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) diff --git a/docs/documentation/platform/identities/alicloud-auth.mdx b/docs/documentation/platform/identities/alicloud-auth.mdx index 2f54ef71b..059adc32d 100644 --- a/docs/documentation/platform/identities/alicloud-auth.mdx +++ b/docs/documentation/platform/identities/alicloud-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/aws-auth.mdx b/docs/documentation/platform/identities/aws-auth.mdx index ab2b5cd3a..1d277a0c0 100644 --- a/docs/documentation/platform/identities/aws-auth.mdx +++ b/docs/documentation/platform/identities/aws-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/azure-auth.mdx b/docs/documentation/platform/identities/azure-auth.mdx index 7a7c112ef..dd73f1783 100644 --- a/docs/documentation/platform/identities/azure-auth.mdx +++ b/docs/documentation/platform/identities/azure-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/gcp-auth.mdx b/docs/documentation/platform/identities/gcp-auth.mdx index 8d6a1f177..cdc4a86a1 100644 --- a/docs/documentation/platform/identities/gcp-auth.mdx +++ b/docs/documentation/platform/identities/gcp-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/jwt-auth.mdx b/docs/documentation/platform/identities/jwt-auth.mdx index 339138881..29a37a5d4 100644 --- a/docs/documentation/platform/identities/jwt-auth.mdx +++ b/docs/documentation/platform/identities/jwt-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/kubernetes-auth.mdx b/docs/documentation/platform/identities/kubernetes-auth.mdx index e357ba75e..f9412b92b 100644 --- a/docs/documentation/platform/identities/kubernetes-auth.mdx +++ b/docs/documentation/platform/identities/kubernetes-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/oci-auth.mdx b/docs/documentation/platform/identities/oci-auth.mdx index ef5fafa4c..e07917c72 100644 --- a/docs/documentation/platform/identities/oci-auth.mdx +++ b/docs/documentation/platform/identities/oci-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/oidc-auth/azure.mdx b/docs/documentation/platform/identities/oidc-auth/azure.mdx index a9f244794..c4dd44152 100644 --- a/docs/documentation/platform/identities/oidc-auth/azure.mdx +++ b/docs/documentation/platform/identities/oidc-auth/azure.mdx @@ -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) diff --git a/docs/documentation/platform/identities/oidc-auth/circleci.mdx b/docs/documentation/platform/identities/oidc-auth/circleci.mdx index bb5999f55..09616a5b4 100644 --- a/docs/documentation/platform/identities/oidc-auth/circleci.mdx +++ b/docs/documentation/platform/identities/oidc-auth/circleci.mdx @@ -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) diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx index f847f51fe..26f291734 100644 --- a/docs/documentation/platform/identities/oidc-auth/general.mdx +++ b/docs/documentation/platform/identities/oidc-auth/general.mdx @@ -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) diff --git a/docs/documentation/platform/identities/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx index 567f38d05..d7c2da280 100644 --- a/docs/documentation/platform/identities/oidc-auth/github.mdx +++ b/docs/documentation/platform/identities/oidc-auth/github.mdx @@ -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) diff --git a/docs/documentation/platform/identities/oidc-auth/gitlab.mdx b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx index b52d2f894..c3aeb9dac 100644 --- a/docs/documentation/platform/identities/oidc-auth/gitlab.mdx +++ b/docs/documentation/platform/identities/oidc-auth/gitlab.mdx @@ -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) diff --git a/docs/documentation/platform/identities/oidc-auth/spire.mdx b/docs/documentation/platform/identities/oidc-auth/spire.mdx index b402a1d10..6fb387391 100644 --- a/docs/documentation/platform/identities/oidc-auth/spire.mdx +++ b/docs/documentation/platform/identities/oidc-auth/spire.mdx @@ -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) diff --git a/docs/documentation/platform/identities/tls-cert-auth.mdx b/docs/documentation/platform/identities/tls-cert-auth.mdx index 0ecb60b99..f52ad3c51 100644 --- a/docs/documentation/platform/identities/tls-cert-auth.mdx +++ b/docs/documentation/platform/identities/tls-cert-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/token-auth.mdx b/docs/documentation/platform/identities/token-auth.mdx index f31e86517..c9a06d110 100644 --- a/docs/documentation/platform/identities/token-auth.mdx +++ b/docs/documentation/platform/identities/token-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 3a87f6da9..18b847a40 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -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) diff --git a/docs/documentation/platform/identities/user-identities.mdx b/docs/documentation/platform/identities/user-identities.mdx index 31e4bf242..0ee4614a4 100644 --- a/docs/documentation/platform/identities/user-identities.mdx +++ b/docs/documentation/platform/identities/user-identities.mdx @@ -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. diff --git a/docs/documentation/platform/kms/overview.mdx b/docs/documentation/platform/kms/overview.mdx index 577373ab8..bf2cf69f0 100644 --- a/docs/documentation/platform/kms/overview.mdx +++ b/docs/documentation/platform/kms/overview.mdx @@ -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). 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 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. @@ -168,7 +168,7 @@ In the following steps, we explore how to use decrypt data using an existing key 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 diff --git a/docs/documentation/platform/pki/ca/acme-ca.mdx b/docs/documentation/platform/pki/ca/acme-ca.mdx index bcdd4f4a9..76f5cdc8f 100644 --- a/docs/documentation/platform/pki/ca/acme-ca.mdx +++ b/docs/documentation/platform/pki/ca/acme-ca.mdx @@ -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) diff --git a/docs/documentation/platform/pki/certificates.mdx b/docs/documentation/platform/pki/certificates.mdx index f1c434e9c..de8de4541 100644 --- a/docs/documentation/platform/pki/certificates.mdx +++ b/docs/documentation/platform/pki/certificates.mdx @@ -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": "", - "name": "My Certificate Template", - "commonName": ".*.acme.com", - "subjectAlternativeName": ".*.acme.com", - "ttl": "1y", - }' + --data '{ + "projectId": "", + "name": "", + "description": "", + "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": "", + "description": "", + "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" + } } ``` - 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": "", + "profileId": "", "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) 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 - 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 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 diff --git a/docs/documentation/platform/pki/certificates/certificates.mdx b/docs/documentation/platform/pki/certificates/certificates.mdx index 3297022e8..05703beee 100644 --- a/docs/documentation/platform/pki/certificates/certificates.mdx +++ b/docs/documentation/platform/pki/certificates/certificates.mdx @@ -206,7 +206,7 @@ openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem - 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 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 diff --git a/docs/documentation/platform/pki/certificates/overview.mdx b/docs/documentation/platform/pki/certificates/overview.mdx index a4688388a..ea559a0df 100644 --- a/docs/documentation/platform/pki/certificates/overview.mdx +++ b/docs/documentation/platform/pki/certificates/overview.mdx @@ -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. diff --git a/docs/documentation/platform/pki/enrollment-methods/api.mdx b/docs/documentation/platform/pki/enrollment-methods/api.mdx index dac7b6386..4adcdc01b 100644 --- a/docs/documentation/platform/pki/enrollment-methods/api.mdx +++ b/docs/documentation/platform/pki/enrollment-methods/api.mdx @@ -56,7 +56,7 @@ Here, select the certificate profile from step 1 that will be used to issue the - 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 diff --git a/docs/documentation/platform/pki/est.mdx b/docs/documentation/platform/pki/est.mdx deleted file mode 100644 index ecbd98dd9..000000000 --- a/docs/documentation/platform/pki/est.mdx +++ /dev/null @@ -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. diff --git a/docs/documentation/platform/pki/integration-guides/apache-certbot.mdx b/docs/documentation/platform/pki/integration-guides/apache-certbot.mdx new file mode 100644 index 000000000..78f0301e1 --- /dev/null +++ b/docs/documentation/platform/pki/integration-guides/apache-certbot.mdx @@ -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 + + + + 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. + + + 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. + + + + + 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 + ``` + + + + 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 + + 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... + + ``` + + 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. + + + + 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) + + + + 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" + ``` + + + 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. + + + + \ No newline at end of file diff --git a/docs/documentation/platform/pki/integration-guides/jboss-certbot.mdx b/docs/documentation/platform/pki/integration-guides/jboss-certbot.mdx new file mode 100644 index 000000000..c0e1c896b --- /dev/null +++ b/docs/documentation/platform/pki/integration-guides/jboss-certbot.mdx @@ -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 + + + + 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. + + + 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. + + + + + 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 + ``` + + + + 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. + + + + 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. + + + 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. + + + + + 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. + + + + 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 + ``` + + + 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. + + + \ No newline at end of file diff --git a/docs/documentation/platform/pki/integration-guides/nginx-certbot.mdx b/docs/documentation/platform/pki/integration-guides/nginx-certbot.mdx new file mode 100644 index 000000000..f28e5ee09 --- /dev/null +++ b/docs/documentation/platform/pki/integration-guides/nginx-certbot.mdx @@ -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 + + + + 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. + + + 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. + + + + + 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 + ``` + + + + 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. + + + + 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) + + + + 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 + ``` + + + + diff --git a/docs/documentation/platform/pki/integration-guides/tomcat-certbot.mdx b/docs/documentation/platform/pki/integration-guides/tomcat-certbot.mdx new file mode 100644 index 000000000..ffb07bf1b --- /dev/null +++ b/docs/documentation/platform/pki/integration-guides/tomcat-certbot.mdx @@ -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 + + + + 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. + + + 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. + + + + + 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 + ``` + + + + 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 + ``` + + + + 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 + + + + + + ``` + + 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. + + + 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. + + + + + 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) + + + + 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 + ``` + + + 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. + + + + + 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. + + + 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. + + + \ No newline at end of file diff --git a/docs/documentation/platform/pki/integration-guides/windows-server-acme.mdx b/docs/documentation/platform/pki/integration-guides/windows-server-acme.mdx new file mode 100644 index 000000000..2aab0870d --- /dev/null +++ b/docs/documentation/platform/pki/integration-guides/windows-server-acme.mdx @@ -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 + + + + 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. + + + 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. + + + + + Install win-acme on your Windows Server using one of the following methods. + + + 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 + ``` + + + 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. + + + + + + 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. + + + 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 + + + + + Win-acme supports various certificate storage options beyond PEM files. Here are common alternatives for different deployment scenarios: + + + + 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 + ``` + + + 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 + ``` + + + 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 + ``` + + + + + + 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. + + + + 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. + + + + + After successful certificate issuance, verify that the certificate files have been created correctly based on your chosen storage method. + + + 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) + + + 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. + + + + diff --git a/docs/documentation/platform/project-templates.mdx b/docs/documentation/platform/project-templates.mdx index 7dd5ceb50..9526f4092 100644 --- a/docs/documentation/platform/project-templates.mdx +++ b/docs/documentation/platform/project-templates.mdx @@ -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. - 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 ' \ --header 'Content-Type: application/json' \ --data '{ "projectName": "My Project", diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx index d11334440..d2e5755c4 100644 --- a/docs/documentation/platform/secret-rotation/overview.mdx +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -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 diff --git a/docs/documentation/platform/secrets-mgmt/project.mdx b/docs/documentation/platform/secrets-mgmt/project.mdx index 3e7c7cbc7..58f50ce37 100644 --- a/docs/documentation/platform/secrets-mgmt/project.mdx +++ b/docs/documentation/platform/secrets-mgmt/project.mdx @@ -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) diff --git a/docs/documentation/platform/ssh/concepts/ssh-certificates.mdx b/docs/documentation/platform/ssh/concepts/ssh-certificates.mdx index 74bfca228..1c22550cd 100644 --- a/docs/documentation/platform/ssh/concepts/ssh-certificates.mdx +++ b/docs/documentation/platform/ssh/concepts/ssh-certificates.mdx @@ -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. diff --git a/docs/documentation/platform/sso/general-oidc/overview.mdx b/docs/documentation/platform/sso/general-oidc/overview.mdx index 586f66f26..5ba469264 100644 --- a/docs/documentation/platform/sso/general-oidc/overview.mdx +++ b/docs/documentation/platform/sso/general-oidc/overview.mdx @@ -77,7 +77,7 @@ Prerequisites: - 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. diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx index 727bf9be6..0a32df228 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -103,7 +103,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." - 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. diff --git a/docs/documentation/platform/workflow-integrations/slack-integration.mdx b/docs/documentation/platform/workflow-integrations/slack-integration.mdx index 2317baabe..38fc15ebe 100644 --- a/docs/documentation/platform/workflow-integrations/slack-integration.mdx +++ b/docs/documentation/platform/workflow-integrations/slack-integration.mdx @@ -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) - 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 + Press **Add** and select **Slack** as the platform. ![project-slack-overview](/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png) @@ -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) - 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**. @@ -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) - 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 + Press **Add** and select **Slack** as the platform. ![project-slack-overview](/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png) @@ -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) + +## 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. + + + + ### 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) + + + + ### 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) + + + + ### 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) + + diff --git a/docs/images/platform/pki/acme/acme-configuration-modal.png b/docs/images/platform/pki/acme/acme-configuration-modal.png new file mode 100644 index 000000000..584a6e643 Binary files /dev/null and b/docs/images/platform/pki/acme/acme-configuration-modal.png differ diff --git a/docs/images/platform/pki/acme/certificate-profile-acme-option.png b/docs/images/platform/pki/acme/certificate-profile-acme-option.png new file mode 100644 index 000000000..463438f20 Binary files /dev/null and b/docs/images/platform/pki/acme/certificate-profile-acme-option.png differ diff --git a/docs/images/platform/pki/integrations/windows-server/certificates-created.png b/docs/images/platform/pki/integrations/windows-server/certificates-created.png new file mode 100644 index 000000000..270e12b1a Binary files /dev/null and b/docs/images/platform/pki/integrations/windows-server/certificates-created.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/access-request-notification.png b/docs/images/platform/workflow-integrations/slack-integration/access-request-notification.png new file mode 100644 index 000000000..ad40c8f4f Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/access-request-notification.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/access-request-updated-notification.png b/docs/images/platform/workflow-integrations/slack-integration/access-request-updated-notification.png new file mode 100644 index 000000000..f7bbf51ae Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/access-request-updated-notification.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credential-form.png b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credential-form.png index 0b97be58a..1af15b074 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credential-form.png and b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credential-form.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credentials.png b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credentials.png index ddf245eff..136cea9d8 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credentials.png and b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-credentials.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-summary.png b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-summary.png index 95e9fe4ba..31cd0c458 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-summary.png and b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-summary.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-workspace-select.png b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-workspace-select.png index 0c047ab1a..39955f547 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-workspace-select.png and b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-app-workspace-select.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-create-app.png b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-create-app.png deleted file mode 100644 index 502dbde0a..000000000 Binary files a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-create-app.png and /dev/null differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-overview.png b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-overview.png index 54ad6ca6e..3a200f296 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-overview.png and b/docs/images/platform/workflow-integrations/slack-integration/admin-slack-integration-overview.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/cloud-org-slack-integration-authenticate.png b/docs/images/platform/workflow-integrations/slack-integration/cloud-org-slack-integration-authenticate.png index 048e91f85..8b9e59eca 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/cloud-org-slack-integration-authenticate.png and b/docs/images/platform/workflow-integrations/slack-integration/cloud-org-slack-integration-authenticate.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-add-form.png b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-add-form.png index 97b38d6e4..1554b9bd8 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-add-form.png and b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-add-form.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-authenticate.png b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-authenticate.png index 166e5849f..ef492c018 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-authenticate.png and b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-authenticate.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-created.png b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-created.png index f46f61d89..bf98abd0e 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-created.png and b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-created.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-initial-add.png b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-initial-add.png index 5bd66d932..5d9316d75 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-initial-add.png and b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-initial-add.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-overview.png b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-overview.png index 1f7386559..9bfbbb7f7 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-overview.png and b/docs/images/platform/workflow-integrations/slack-integration/org-slack-integration-overview.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png index 3f6bd0d1d..4864045f3 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png and b/docs/images/platform/workflow-integrations/slack-integration/private-slack-setup-channel-field.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-config.png b/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-config.png index c6dd70ad7..a844988d5 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-config.png and b/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-config.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png b/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png index 944db9cca..6806527da 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png and b/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-overview.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-select.png b/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-select.png index 073d3fd93..9367d55a5 100644 Binary files a/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-select.png and b/docs/images/platform/workflow-integrations/slack-integration/project-slack-integration-select.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/secret-approval-notification.png b/docs/images/platform/workflow-integrations/slack-integration/secret-approval-notification.png new file mode 100644 index 000000000..ef6d2ddad Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/secret-approval-notification.png differ diff --git a/docs/images/platform/workflow-integrations/slack-integration/secret-sync-error-notification.png b/docs/images/platform/workflow-integrations/slack-integration/secret-sync-error-notification.png new file mode 100644 index 000000000..121864df6 Binary files /dev/null and b/docs/images/platform/workflow-integrations/slack-integration/secret-sync-error-notification.png differ diff --git a/docs/integrations/platforms/docker-swarm-with-agent.mdx b/docs/integrations/platforms/docker-swarm-with-agent.mdx index 30118a8f0..40d9986bb 100644 --- a/docs/integrations/platforms/docker-swarm-with-agent.mdx +++ b/docs/integrations/platforms/docker-swarm-with-agent.mdx @@ -4,7 +4,7 @@ description: "Learn how to manage secrets in Docker Swarm services." --- In this guide, we'll demonstrate how to use Infisical for managing secrets within Docker Swarm. -Specifically, we'll set up a sidecar container using the [Infisical Agent](/infisical-agent/overview), which authenticates with Infisical to retrieve secrets and access tokens. +Specifically, we'll set up a sidecar container using the [Infisical Agent](/integrations/platforms/infisical-agent), which authenticates with Infisical to retrieve secrets and access tokens. These secrets are then stored in a shared volume accessible by other services in your Docker Swarm. ## Prerequisites @@ -12,7 +12,7 @@ These secrets are then stored in a shared volume accessible by other services in - Docker version 20.10.24 or newer - Basic knowledge of Docker Swarm - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed on your system -- Familiarity with the [Infisical Agent](/infisical-agent/overview) +- Familiarity with the [Infisical Agent](/integrations/platforms/infisical-agent) ## Objective Our goal is to deploy an Nginx instance in your Docker Swarm cluster, configured to display Infisical secrets on its landing page. This will provide hands-on experience in fetching and utilizing secrets from Infisical within Docker Swarm. The principles demonstrated here are also applicable to Docker Compose deployments. diff --git a/docs/integrations/platforms/ecs-with-agent.mdx b/docs/integrations/platforms/ecs-with-agent.mdx index 31c5a982b..b6b9ce7f2 100644 --- a/docs/integrations/platforms/ecs-with-agent.mdx +++ b/docs/integrations/platforms/ecs-with-agent.mdx @@ -7,7 +7,7 @@ description: "Learn how to deliver secrets to Amazon Elastic Container Service." This guide will go over the steps needed to access secrets stored in Infisical from Amazon Elastic Container Service (ECS). -At a high level, the steps involve setting up an ECS task with an [Infisical Agent](/infisical-agent/overview) as a sidecar container. This sidecar container uses [AWS Auth](/documentation/platform/identities/aws-auth) to authenticate with Infisical to fetch secrets/access tokens. +At a high level, the steps involve setting up an ECS task with an [Infisical Agent](/integrations/platforms/infisical-agent) as a sidecar container. This sidecar container uses [AWS Auth](/documentation/platform/identities/aws-auth) to authenticate with Infisical to fetch secrets/access tokens. Once the secrets/access tokens are retrieved, they are then stored in a shared [Amazon Elastic File System](https://aws.amazon.com/efs/) (EFS) volume. This volume is then made accessible to your application and all of its replicas. This guide primarily focuses on integrating Infisical Cloud with Amazon ECS on AWS Fargate and Amazon EFS. @@ -21,7 +21,7 @@ This guide requires the following prerequisites: - Git installed - Terraform v1.0 or later installed - Access to AWS credentials -- Understanding of [Infisical Agent](/infisical-agent/overview) +- Understanding of [Infisical Agent](/integrations/platforms/infisical-agent) ## What we will deploy diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 848da4055..f2911ac2f 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -19,7 +19,7 @@ This CRD offers the following features: ### Prerequisites - A project within Infisical. -- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create dynamic secret leases in the project. +- A [machine identity](/documentation/platform/identities/machine-identities) ready for use in Infisical that has permissions to create dynamic secret leases in the project. - You have already configured a dynamic secret in Infisical. - The operator is installed on to your Kubernetes cluster. diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx index 0affe7841..a5c68e503 100644 --- a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -17,7 +17,7 @@ This CRD offers the following features: ### Prerequisites - A project within Infisical. -- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create secrets in your project. +- A [machine identity](/documentation/platform/identities/machine-identities) ready for use in Infisical that has permissions to create secrets in your project. - The operator is installed on to your Kubernetes cluster. ## Example usage diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index d7fb6249a..a7f3dd4ce 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -256,7 +256,7 @@ spec: ![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) @@ -432,7 +432,7 @@ spec: ![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) @@ -803,7 +803,7 @@ Follow the instructions below to create and store the service token in a Kuberne #### 1. Generate service token -You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. +You can generate a [service token](/documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. #### 2. Create Kubernetes secret containing service token diff --git a/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx b/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx index 91657c760..56c047056 100644 --- a/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx +++ b/docs/integrations/secret-syncs/digital-ocean-app-platform.mdx @@ -77,7 +77,7 @@ description: "Learn how to configure a DigitalOcean App Platform Sync for Infisi - To create a **DigitalOcean App Platform Sync**, make an API request to the [Create DigitalOcean Sync](/api-reference/endpoints/secret-syncs/digital-ocean/create) API endpoint. + To create a **DigitalOcean App Platform Sync**, make an API request to the [Create DigitalOcean Sync](/api-reference/endpoints/secret-syncs/digital-ocean-app-platform/create) API endpoint. ### Sample request diff --git a/docs/internals/overview.mdx b/docs/internals/overview.mdx index 510a6b06c..fc58faca2 100644 --- a/docs/internals/overview.mdx +++ b/docs/internals/overview.mdx @@ -15,7 +15,7 @@ This section covers the internals of Infisical including its technical underpinn We do not recommend hardcoding your [Machine Identity - Tokens](/platform/identities/overview). Setting it as an environment variable + Tokens](/documentation/platform/identities/machine-identities). Setting it as an environment variable would be best. diff --git a/docs/sdks/languages/ruby.mdx b/docs/sdks/languages/ruby.mdx index 3fb9cb3e1..c5417fcdc 100644 --- a/docs/sdks/languages/ruby.mdx +++ b/docs/sdks/languages/ruby.mdx @@ -36,7 +36,7 @@ puts "Secret: #{single_test_secret}" This example demonstrates how to use the Infisical Ruby SDK in a simple Ruby application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project. - We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best. + We do not recommend hardcoding your [Machine Identity Tokens](/documentation/platform/identities/machine-identities). Setting it as an environment variable would be best. # Installation diff --git a/docs/self-hosting/deployment-options/native/standalone-binary.mdx b/docs/self-hosting/deployment-options/native/standalone-binary.mdx deleted file mode 100644 index 5767ca948..000000000 --- a/docs/self-hosting/deployment-options/native/standalone-binary.mdx +++ /dev/null @@ -1,202 +0,0 @@ ---- -title: "Standalone" -description: "Learn how to deploy Infisical in a standalone environment." ---- - -# Self-Hosting Infisical with Standalone Infisical - -Deploying Infisical in a standalone environment is a great way to get started with Infisical without having to use containers. This guide will walk you through the process of deploying Infisical in a standalone environment. -This is one of the easiest ways to deploy Infisical. It is a single executable, currently only supported on Debian-based systems. - -The standalone deployment implements the "bring your own database" (BYOD) approach. This means that you will need to provide your own databases (specifically Postgres and Redis) for the Infisical services to use. The standalone deployment does not include any databases. - -If you wish to streamline the deployment process, we recommend using the Ansible role for Infisical. The Ansible role automates the end to end deployment process, and will take care of everything like databases, redis deployment, web serving, and availability. -- [Automated Deployment with high availability (HA)](/self-hosting/deployment-options/native/high-availability) - - -## Prerequisites -- A server running a Debian-based operating system (e.g., Ubuntu, Debian) -- A Postgres database -- A Redis database - -## Installing Infisical -Installing Infisical is as simple as running a single command. You can install Infisical by running the following command: - -```bash - $ curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-core/cfg/setup/bash.deb.sh' | sudo bash && sudo apt-get install -y infisical-core -``` - -## Running Infisical -Running Infisical and serving it to the web has a few steps. Below are the steps to get you started with running Infisical in a standalone environment. - * Setup environment variables - * Running Postgres migrations - * Create system daemon - * Exposing Infisical to the internet - - - - - To use Infisical you'll need to configure the environment variables beforehand. You can acheive this by creating an environment file to be used by Infisical. - - - #### Create environment file - ```bash - $ mkdir -p /etc/infisical && touch /etc/infisical/environment - ``` - - After creating the environment file, you'll need to fill it out with your environment variables. - - #### Edit environment file - ```bash - $ nano /etc/infisical/environment - ``` - - ```bash - DB_CONNECTION_URI=postgres://user:password@localhost:5432/infisical # Replace with your Postgres database connection URI - REDIS_URL=redis://localhost:6379 # Replace with your Redis connection URI - ENCRYPTION_KEY=your_encryption_key # Replace with your encryption key (can be generated with: openssl rand -hex 16) - AUTH_SECRET=your_auth_secret # Replace with your auth secret (can be generated with: openssl rand -base64 32) - ``` - - - The minimum required environment variables are `DB_CONNECTION_URI`, `REDIS_URL`, `ENCRYPTION_KEY`, and `AUTH_SECRET`. We recommend You take a look at our [list of all available environment variables](/docs/self-hosting/configuration/envars#general-platform), and configure the ones you need. - - - - - Assuming you're starting with a fresh Postgres database, you'll need to run the Postgres migrations to syncronize the database schema. - The migration command will use the environment variables you configured in the previous step. - - - ```bash - $ eval $(cat /etc/infisical/environment) infisical-core migration:latest - ``` - - - This step will need to be repeated if you update Infisical in the future. - - - - - - ```bash - $ nano /etc/systemd/system/infisical.service - ``` - - - - Create a systemd service file for Infisical. Creating a systemd service file will allow Infisical to start automatically when the system boots or in case of a crash. - - ```bash - $ nano /etc/systemd/system/infisical.service - ``` - - ```ini - [Unit] - Description=Infisical Service - After=network.target - - [Service] - # The path to the environment file we created in the previous step - EnvironmentFile=/etc/infisical/environment - Type=simple - # Change the user to the user you want to run Infisical as - User=root - ExecStart=/usr/local/bin/infisical-core - Restart=always - RestartSec=30 - - [Install] - WantedBy=multi-user.target - ``` - - Now we need to reload the systemd daemon and start the Infisical service. - - ```bash - $ systemctl daemon-reload - $ systemctl start infisical - $ systemctl enable infisical - ``` - - - You can check the status of the Infisical service by running `systemctl status infisical`. - It is also a good idea to check the logs for any errors by running `journalctl --no-pager -u infisical`. - - - - Exposing Infisical to the internet requires setting up a reverse proxy. You can use any reverse proxy of your choice, but we recommend using HAProxy or Nginx. Below is an example of how to set up a reverse proxy using HAProxy. - - #### Install HAProxy - ```bash - $ apt-get install -y haproxy - ``` - - #### Edit HAProxy configuration - ```bash - $ nano /etc/haproxy/haproxy.cfg - ``` - - ```ini - global - log /dev/log local0 - log /dev/log local1 notice - chroot /var/lib/haproxy - stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners - stats timeout 30s - user haproxy - group haproxy - daemon - - defaults - log global - mode http - option httplog - option dontlognull - timeout connect 5000 - timeout client 50000 - timeout server 50000 - - frontend http-in - bind *:80 - default_backend infisical - - backend infisical - server infisicalapp 127.0.0.1:8080 check - ``` - - - If you decide to use Nginx, then please be aware that the configuration will be different. **Infisical listens on port 8080**. - - - #### Restart HAProxy - ```bash - $ systemctl restart haproxy - ``` - - - - -And that's it! You have successfully deployed Infisical in a standalone environment. You can now access Infisical by visiting `http://your-server-ip`. - - - Please take note that the Infisical team cannot provide infrastructure support for **free self-hosted** deployments.
If you need help with infrastructure, we recommend upgrading to a [paid plan](https://infisical.com/pricing) which includes infrastructure support. - - You can also join our community [Slack](https://infisical.com/slack) for help and support from the community. -
- -## Troubleshooting - - - This is a common issue related to the HAProxy configuration file. The error is caused by the missing newline character at the end of the file. You can fix this by adding a newline character at the end of the file. - - ```bash - $ echo "" >> /etc/haproxy/haproxy.cfg - ``` - - - This issue can be caused by a number of reasons, mostly realted to the network configuration. Here are a few things you can check: - 1. Ensure that the firewall is not blocking the connection. You can check this by running `ufw status`. Ensure that port 80 is open. - 2. If you're using a cloud provider like AWS or GCP, ensure that the security group allows traffic on port 80. - 3. Ensure that the HAProxy service is running. You can check this by running `systemctl status haproxy`. - 4. Ensure that the Infisical service is running. You can check this by running `systemctl status infisical`. - \ No newline at end of file diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index acb692711..040bfa6b7 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -11,7 +11,7 @@ Choose from a number of deployment options listed below to get started. title="Docker" color="#000000" icon="docker" - href="deployment-options/standalone-infisical" + href="./deployment-options/standalone-infisical" > Use the fully packaged docker image to deploy Infisical anywhere.
@@ -20,7 +20,7 @@ Choose from a number of deployment options listed below to get started. title="Docker Compose" color="#000000" icon="docker" - href="deployment-options/docker-compose" + href="./deployment-options/docker-compose" > Install Infisical using our Docker Compose template. @@ -28,7 +28,7 @@ Choose from a number of deployment options listed below to get started. title="Kubernetes" color="#000000" icon="gear-complex-code" - href="deployment-options/kubernetes-helm" + href="./deployment-options/kubernetes-helm" > Use our Helm chart to Install Infisical on your Kubernetes cluster. @@ -36,7 +36,7 @@ Choose from a number of deployment options listed below to get started. Install Infisical on your system without containers using our Linux package. diff --git a/frontend/src/hooks/api/secretSharing/types.ts b/frontend/src/hooks/api/secretSharing/types.ts index c35228fab..524346beb 100644 --- a/frontend/src/hooks/api/secretSharing/types.ts +++ b/frontend/src/hooks/api/secretSharing/types.ts @@ -59,6 +59,8 @@ export type TViewSharedSecretResponse = { tag: string; accessType: SecretSharingAccessType; orgName?: string; + expiresAt?: Date | string; + expiresAfterViews?: number | null; }; }; diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx index ec660b339..33f92a122 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CertificateTemplatesV2Tab.tsx @@ -10,7 +10,7 @@ import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types"; import { useDeleteCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/mutations"; -import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types"; +import { type TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types"; import { CreateTemplateModal } from "./CreateTemplateModal"; import { TemplateList } from "./TemplateList"; @@ -84,7 +84,12 @@ export const CertificateTemplatesV2Tab = () => { - setIsCreateModalOpen(false)} /> + { + setIsCreateModalOpen(false); + }} + /> {selectedTemplate && ( <> diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx index b929ce36e..d55c1b5ef 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx @@ -36,13 +36,18 @@ import { CertDurationUnit, CertExtendedKeyUsageType, CertKeyUsageType, + CertSanInclude, CertSubjectAlternativeNameType, + CertSubjectAttributeInclude, CertSubjectAttributeType, SAN_INCLUDE_OPTIONS, SAN_TYPE_OPTIONS, SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS, - SUBJECT_ATTRIBUTE_TYPE_OPTIONS + SUBJECT_ATTRIBUTE_TYPE_OPTIONS, + TEMPLATE_PRESET_IDS, + type TemplatePresetId } from "./shared/certificate-constants"; +import { CERTIFICATE_TEMPLATE_PRESETS } from "./shared/template-presets"; import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared"; export type FormData = TemplateFormData; @@ -91,7 +96,7 @@ const SIGNATURE_ALGORITHMS = [ "SHA256-ECDSA", "SHA384-ECDSA", "SHA512-ECDSA" -]; +] as const; const KEY_ALGORITHMS = [ "RSA-2048", @@ -100,7 +105,7 @@ const KEY_ALGORITHMS = [ "ECDSA-P256", "ECDSA-P384", "ECDSA-P521" -]; +] as const; export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }: Props) => { const { currentProject } = useProject(); @@ -117,7 +122,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" subj.allowed.forEach((allowedValue) => { attributes.push({ type: subj.type as CertSubjectAttributeType, - include: "optional", + include: CertSubjectAttributeInclude.OPTIONAL, value: [allowedValue] }); }); @@ -126,7 +131,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" subj.denied.forEach((deniedValue) => { attributes.push({ type: subj.type as CertSubjectAttributeType, - include: "prohibit", + include: CertSubjectAttributeInclude.PROHIBIT, value: [deniedValue] }); }); @@ -141,7 +146,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" san.required.forEach((requiredValue) => { subjectAlternativeNames.push({ type: san.type as CertSubjectAlternativeNameType, - include: "mandatory", + include: CertSanInclude.MANDATORY, value: [requiredValue] }); }); @@ -150,7 +155,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" san.allowed.forEach((allowedValue) => { subjectAlternativeNames.push({ type: san.type as CertSubjectAlternativeNameType, - include: "optional", + include: CertSanInclude.OPTIONAL, value: [allowedValue] }); }); @@ -159,7 +164,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" san.denied.forEach((deniedValue) => { subjectAlternativeNames.push({ type: san.type as CertSubjectAlternativeNameType, - include: "prohibit", + include: CertSanInclude.PROHIBIT, value: [deniedValue] }); }); @@ -219,6 +224,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; return { + preset: TEMPLATE_PRESET_IDS.CUSTOM, name: templateData.name || "", description: templateData.description || "", attributes, @@ -231,25 +237,30 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }; }; - const getDefaultValues = (): FormData => ({ - name: "", - description: "", - attributes: [], - keyUsages: { requiredUsages: [], optionalUsages: [] }, - extendedKeyUsages: { requiredUsages: [], optionalUsages: [] }, - subjectAlternativeNames: [], - validity: { - maxDuration: { value: 365, unit: CertDurationUnit.DAYS } - }, - signatureAlgorithm: { - allowedAlgorithms: [] - }, - keyAlgorithm: { - allowedKeyTypes: [] - } - }); + const getDefaultValues = (): FormData & { preset: TemplatePresetId } => { + return { + preset: TEMPLATE_PRESET_IDS.CUSTOM, + name: "", + description: "", + attributes: [], + keyUsages: { requiredUsages: [], optionalUsages: [] }, + extendedKeyUsages: { requiredUsages: [], optionalUsages: [] }, + subjectAlternativeNames: [], + validity: { + maxDuration: { value: 365, unit: CertDurationUnit.DAYS } + }, + signatureAlgorithm: { + allowedAlgorithms: [] + }, + keyAlgorithm: { + allowedKeyTypes: [] + } + }; + }; - const { control, handleSubmit, reset, watch, setValue, formState } = useForm({ + const { control, handleSubmit, reset, watch, setValue, formState } = useForm< + FormData & { preset: TemplatePresetId } + >({ resolver: zodResolver(templateSchema), defaultValues: getDefaultValues(), mode: "onChange", @@ -260,7 +271,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" useEffect(() => { if (isEdit && template) { const convertedData = convertApiToUiFormat(template); - reset(convertedData); + reset({ ...convertedData, preset: TEMPLATE_PRESET_IDS.CUSTOM }); } else if (!isEdit) { reset(getDefaultValues()); } @@ -273,6 +284,37 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" requiredUsages: [], optionalUsages: [] }; + const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM; + + const handlePresetChange = (presetId: TemplatePresetId) => { + setValue("preset", presetId); + + if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) { + return; + } + + const selectedPreset = CERTIFICATE_TEMPLATE_PRESETS.find((p) => p.id === presetId); + if (selectedPreset) { + if (selectedPreset.formData.keyUsages) { + setValue("keyUsages", selectedPreset.formData.keyUsages); + } + if (selectedPreset.formData.extendedKeyUsages) { + setValue("extendedKeyUsages", selectedPreset.formData.extendedKeyUsages); + } + if (selectedPreset.formData.attributes) { + setValue("attributes", selectedPreset.formData.attributes); + } + if (selectedPreset.formData.subjectAlternativeNames) { + setValue("subjectAlternativeNames", selectedPreset.formData.subjectAlternativeNames); + } + if (selectedPreset.formData.signatureAlgorithm) { + setValue("signatureAlgorithm", selectedPreset.formData.signatureAlgorithm); + } + if (selectedPreset.formData.keyAlgorithm) { + setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm); + } + } + }; const consolidateByType = < T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] } @@ -309,9 +351,17 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" data.attributes?.map((attr) => { const result: AttributeTransform = { type: attr.type }; - if (attr.include === "optional" && attr.value && attr.value.length > 0) { + if ( + attr.include === CertSubjectAttributeInclude.OPTIONAL && + attr.value && + attr.value.length > 0 + ) { result.allowed = attr.value; - } else if (attr.include === "prohibit" && attr.value && attr.value.length > 0) { + } else if ( + attr.include === CertSubjectAttributeInclude.PROHIBIT && + attr.value && + attr.value.length > 0 + ) { result.denied = attr.value; } @@ -322,11 +372,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" data.subjectAlternativeNames?.map((san) => { const result: SanTransform = { type: san.type }; - if (san.include === "mandatory" && san.value && san.value.length > 0) { + if (san.include === CertSanInclude.MANDATORY && san.value && san.value.length > 0) { result.required = san.value; - } else if (san.include === "optional" && san.value && san.value.length > 0) { + } else if (san.include === CertSanInclude.OPTIONAL && san.value && san.value.length > 0) { result.allowed = san.value; - } else if (san.include === "prohibit" && san.value && san.value.length > 0) { + } else if (san.include === CertSanInclude.PROHIBIT && san.value && san.value.length > 0) { result.denied = san.value; } @@ -464,11 +514,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" value: ["*"] }; setValue("attributes", [...watchedAttributes, newAttribute]); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }; const removeAttribute = (index: number) => { const newAttributes = watchedAttributes.filter((_, i) => i !== index); setValue("attributes", newAttributes); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }; const addSan = () => { @@ -478,11 +536,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" value: ["*"] }; setValue("subjectAlternativeNames", [...watchedSans, newSan]); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }; const removeSan = (index: number) => { const newSans = watchedSans.filter((_, i) => i !== index); setValue("subjectAlternativeNames", newSans); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }; const handleKeyUsagesChange = (usages: { @@ -493,6 +559,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" requiredUsages: usages.requiredUsages, optionalUsages: usages.optionalUsages }); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }; const handleExtendedKeyUsagesChange = (usages: { @@ -503,6 +573,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" requiredUsages: usages.requiredUsages, optionalUsages: usages.optionalUsages }); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }; return ( @@ -555,6 +629,33 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" )} /> + + ( + + + + )} + /> Subject Attributes @@ -595,6 +696,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" type: value as CertSubjectAttributeType }; setValue("attributes", newAttributes); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }} className="w-48" > @@ -615,6 +720,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" value as (typeof SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS)[number] }; setValue("attributes", newAttributes); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }} className="w-32" > @@ -635,6 +744,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" value: e.target.value.trim() ? [e.target.value.trim()] : [] }; setValue("attributes", newAttributes); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }} className={`flex-1 ${ attr.value && attr.value.length > 0 && attr.value[0] === "" @@ -701,6 +814,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" type: value as CertSubjectAlternativeNameType }; setValue("subjectAlternativeNames", newSans); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }} className="w-36" > @@ -720,6 +837,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" include: value as (typeof SAN_INCLUDE_OPTIONS)[number] }; setValue("subjectAlternativeNames", newSans); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }} className="w-32" > @@ -740,6 +861,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" value: e.target.value.trim() ? [e.target.value.trim()] : [] }; setValue("subjectAlternativeNames", newSans); + + if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) { + setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM); + } }} className={`flex-1 ${ san.value && san.value.length > 0 && san.value[0] === "" diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants.ts b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants.ts index 50b69a2e2..47fdcd5a2 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants.ts +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/certificate-constants.ts @@ -150,8 +150,19 @@ export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttribute export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule); export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect); -export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = ["optional", "prohibit"] as const; -export const SAN_INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const; +export enum CertSubjectAttributeInclude { + OPTIONAL = "optional", + PROHIBIT = "prohibit" +} + +export enum CertSanInclude { + MANDATORY = "mandatory", + OPTIONAL = "optional", + PROHIBIT = "prohibit" +} + +export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = Object.values(CertSubjectAttributeInclude); +export const SAN_INCLUDE_OPTIONS = Object.values(CertSanInclude); export const USAGE_STATES = { REQUIRED: "required", @@ -239,3 +250,27 @@ export const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string => }; return mapping[templateFormat] || templateFormat; }; + +export const TEMPLATE_PRESET_IDS = { + CUSTOM: "custom", + TLS_SERVER: "tls-server", + TLS_CLIENT: "tls-client", + CODE_SIGNING: "code-signing", + DEVICE: "device", + USER: "user", + EMAIL_PROTECTION: "email-protection", + DUAL_PURPOSE_SERVER: "dual-purpose-server" +} as const; + +export type TemplatePresetId = (typeof TEMPLATE_PRESET_IDS)[keyof typeof TEMPLATE_PRESET_IDS]; + +export const ALGORITHM_FAMILIES = { + ECDSA: { + signature: ["SHA256-ECDSA", "SHA384-ECDSA", "SHA512-ECDSA"] as const, + key: ["ECDSA-P256", "ECDSA-P384", "ECDSA-P521"] as const + }, + RSA: { + signature: ["SHA256-RSA", "SHA384-RSA", "SHA512-RSA"] as const, + key: ["RSA-2048", "RSA-3072", "RSA-4096"] as const + } +} as const; diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts index 0590c4160..137ca262c 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts @@ -4,21 +4,22 @@ import { CertDurationUnit, CertExtendedKeyUsageType, CertKeyUsageType, + CertSanInclude, CertSubjectAlternativeNameType, + CertSubjectAttributeInclude, CertSubjectAttributeType, - SAN_INCLUDE_OPTIONS, - SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS + TEMPLATE_PRESET_IDS } from "./certificate-constants"; export const uiAttributeSchema = z.object({ type: z.nativeEnum(CertSubjectAttributeType), - include: z.enum(SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS), + include: z.nativeEnum(CertSubjectAttributeInclude), value: z.array(z.string().min(1, "Value cannot be empty")) }); export const uiSanSchema = z.object({ type: z.nativeEnum(CertSubjectAlternativeNameType), - include: z.enum(SAN_INCLUDE_OPTIONS), + include: z.nativeEnum(CertSanInclude), value: z.array(z.string().min(1, "Value cannot be empty")) }); @@ -51,7 +52,21 @@ export const uiKeyAlgorithmSchema = z.object({ .min(1, "At least one key type must be selected") }); +export const uiPresetSchema = z + .enum([ + TEMPLATE_PRESET_IDS.CUSTOM, + TEMPLATE_PRESET_IDS.TLS_SERVER, + TEMPLATE_PRESET_IDS.TLS_CLIENT, + TEMPLATE_PRESET_IDS.CODE_SIGNING, + TEMPLATE_PRESET_IDS.DEVICE, + TEMPLATE_PRESET_IDS.USER, + TEMPLATE_PRESET_IDS.EMAIL_PROTECTION, + TEMPLATE_PRESET_IDS.DUAL_PURPOSE_SERVER + ]) + .default(TEMPLATE_PRESET_IDS.CUSTOM); + export const templateSchema = z.object({ + preset: uiPresetSchema, name: z .string() .trim() diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/template-presets.ts b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/template-presets.ts new file mode 100644 index 000000000..9edad21ea --- /dev/null +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/template-presets.ts @@ -0,0 +1,385 @@ +import { + ALGORITHM_FAMILIES, + CertDurationUnit, + CertExtendedKeyUsageType, + CertKeyUsageType, + CertSanInclude, + CertSubjectAlternativeNameType, + CertSubjectAttributeInclude, + CertSubjectAttributeType, + TEMPLATE_PRESET_IDS, + type TemplatePresetId +} from "./certificate-constants"; +import { TemplateFormData } from "."; + +export interface CertificateTemplatePreset { + readonly id: TemplatePresetId; + readonly name: string; + readonly description: string; + readonly useCase: string; + readonly formData: Omit; +} + +export const CERTIFICATE_TEMPLATE_PRESETS: CertificateTemplatePreset[] = [ + { + id: TEMPLATE_PRESET_IDS.TLS_SERVER, + name: "TLS Server Certificate", + description: "Standard TLS/SSL server certificate for HTTPS services and API endpoints.", + useCase: "Web servers, API endpoints, HTTPS services", + formData: { + name: "TLS Server Certificate", + description: "Standard TLS/SSL server certificate for HTTPS services and API endpoints.", + keyUsages: { + requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE], + optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_ENCIPHERMENT] + }, + extendedKeyUsages: { + requiredUsages: [CertExtendedKeyUsageType.SERVER_AUTH], + optionalUsages: [CertExtendedKeyUsageType.SERVER_AUTH] + }, + validity: { + maxDuration: { + value: 365, + unit: CertDurationUnit.DAYS + } + }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertSubjectAttributeInclude.OPTIONAL, + value: ["*"] + } + ], + subjectAlternativeNames: [ + { + type: CertSubjectAlternativeNameType.DNS_NAME, + include: CertSanInclude.OPTIONAL, + value: ["*"] + }, + { + type: CertSubjectAlternativeNameType.IP_ADDRESS, + include: CertSanInclude.OPTIONAL, + value: ["*"] + } + ], + signatureAlgorithm: { + allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature] + }, + keyAlgorithm: { + allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key] + } + } + }, + { + id: TEMPLATE_PRESET_IDS.TLS_CLIENT, + name: "TLS Client Certificate", + description: "Client certificate for mutual TLS authentication and API access.", + useCase: "Client authentication, mTLS, API authentication", + formData: { + name: "TLS Client Certificate", + description: "Client certificate for mutual TLS authentication and API access.", + keyUsages: { + requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE], + optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_AGREEMENT] + }, + extendedKeyUsages: { + requiredUsages: [CertExtendedKeyUsageType.CLIENT_AUTH], + optionalUsages: [CertExtendedKeyUsageType.CLIENT_AUTH] + }, + validity: { + maxDuration: { + value: 365, + unit: CertDurationUnit.DAYS + } + }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertSubjectAttributeInclude.OPTIONAL, + value: ["*"] + } + ], + subjectAlternativeNames: [ + { + type: CertSubjectAlternativeNameType.EMAIL, + include: CertSanInclude.OPTIONAL, + value: ["*"] + }, + { + type: CertSubjectAlternativeNameType.DNS_NAME, + include: CertSanInclude.OPTIONAL, + value: ["*"] + } + ], + signatureAlgorithm: { + allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature] + }, + keyAlgorithm: { + allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key] + } + } + }, + { + id: TEMPLATE_PRESET_IDS.CODE_SIGNING, + name: "Code Signing Certificate", + description: + "Certificate for signing software, executables, and packages. Requires hardware security modules.", + useCase: "Software signing, executable authentication, package validation", + formData: { + name: "Code Signing Certificate", + description: + "Certificate for signing software, executables, and packages. Requires hardware security modules.", + keyUsages: { + requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.NON_REPUDIATION], + optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.NON_REPUDIATION] + }, + extendedKeyUsages: { + requiredUsages: [CertExtendedKeyUsageType.CODE_SIGNING], + optionalUsages: [ + CertExtendedKeyUsageType.CODE_SIGNING, + CertExtendedKeyUsageType.TIME_STAMPING + ] + }, + validity: { + maxDuration: { + value: 365, + unit: CertDurationUnit.DAYS + } + }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertSubjectAttributeInclude.OPTIONAL, + value: ["*"] + } + ], + subjectAlternativeNames: [ + { + type: CertSubjectAlternativeNameType.EMAIL, + include: CertSanInclude.OPTIONAL, + value: ["*"] + } + ], + signatureAlgorithm: { + allowedAlgorithms: [...ALGORITHM_FAMILIES.RSA.signature] + }, + keyAlgorithm: { + allowedKeyTypes: [...ALGORITHM_FAMILIES.RSA.key] + } + } + }, + { + id: TEMPLATE_PRESET_IDS.DEVICE, + name: "Device Certificate", + description: + "Certificate for IoT devices and embedded systems authentication. IEEE 802.1AR compliant.", + useCase: "Device authentication, IoT security, embedded systems", + formData: { + name: "Device Certificate", + description: + "Certificate for IoT devices and embedded systems authentication. IEEE 802.1AR compliant.", + keyUsages: { + requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE], + optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_AGREEMENT] + }, + extendedKeyUsages: { + requiredUsages: [CertExtendedKeyUsageType.CLIENT_AUTH], + optionalUsages: [CertExtendedKeyUsageType.CLIENT_AUTH, CertExtendedKeyUsageType.SERVER_AUTH] + }, + validity: { + maxDuration: { + value: 365, + unit: CertDurationUnit.DAYS + } + }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertSubjectAttributeInclude.OPTIONAL, + value: ["*"] + } + ], + subjectAlternativeNames: [ + { + type: CertSubjectAlternativeNameType.DNS_NAME, + include: CertSanInclude.OPTIONAL, + value: ["*"] + }, + { + type: CertSubjectAlternativeNameType.IP_ADDRESS, + include: CertSanInclude.OPTIONAL, + value: ["*"] + } + ], + signatureAlgorithm: { + allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature] + }, + keyAlgorithm: { + allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key] + } + } + }, + { + id: TEMPLATE_PRESET_IDS.USER, + name: "User Certificate", + description: + "Personal certificate for user authentication and email signing. FIPS 201 PIV compliant.", + useCase: "Personal authentication, smart cards, email protection", + formData: { + name: "User Certificate", + description: + "Personal certificate for user authentication and email signing. FIPS 201 PIV compliant.", + keyUsages: { + requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE], + optionalUsages: [ + CertKeyUsageType.DIGITAL_SIGNATURE, + CertKeyUsageType.KEY_ENCIPHERMENT, + CertKeyUsageType.KEY_AGREEMENT + ] + }, + extendedKeyUsages: { + requiredUsages: [ + CertExtendedKeyUsageType.CLIENT_AUTH, + CertExtendedKeyUsageType.EMAIL_PROTECTION + ], + optionalUsages: [ + CertExtendedKeyUsageType.CLIENT_AUTH, + CertExtendedKeyUsageType.EMAIL_PROTECTION + ] + }, + validity: { + maxDuration: { + value: 365, + unit: CertDurationUnit.DAYS + } + }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertSubjectAttributeInclude.OPTIONAL, + value: ["*"] + } + ], + subjectAlternativeNames: [ + { + type: CertSubjectAlternativeNameType.EMAIL, + include: CertSanInclude.OPTIONAL, + value: ["*"] + } + ], + signatureAlgorithm: { + allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature] + }, + keyAlgorithm: { + allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key] + } + } + }, + { + id: TEMPLATE_PRESET_IDS.EMAIL_PROTECTION, + name: "Email Protection Certificate", + description: "S/MIME certificate for email encryption and digital signing. RFC 8550 compliant.", + useCase: "Email encryption, digital signing, secure messaging", + formData: { + name: "Email Protection Certificate", + description: + "S/MIME certificate for email encryption and digital signing. RFC 8550 compliant.", + keyUsages: { + requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE], + optionalUsages: [ + CertKeyUsageType.DIGITAL_SIGNATURE, + CertKeyUsageType.KEY_ENCIPHERMENT, + CertKeyUsageType.KEY_AGREEMENT + ] + }, + extendedKeyUsages: { + requiredUsages: [CertExtendedKeyUsageType.EMAIL_PROTECTION], + optionalUsages: [CertExtendedKeyUsageType.EMAIL_PROTECTION] + }, + validity: { + maxDuration: { + value: 365, + unit: CertDurationUnit.DAYS + } + }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertSubjectAttributeInclude.OPTIONAL, + value: ["*"] + } + ], + subjectAlternativeNames: [ + { + type: CertSubjectAlternativeNameType.EMAIL, + include: CertSanInclude.OPTIONAL, + value: ["*"] + } + ], + signatureAlgorithm: { + allowedAlgorithms: [...ALGORITHM_FAMILIES.RSA.signature] + }, + keyAlgorithm: { + allowedKeyTypes: [...ALGORITHM_FAMILIES.RSA.key] + } + } + }, + { + id: TEMPLATE_PRESET_IDS.DUAL_PURPOSE_SERVER, + name: "Dual-Purpose Server Certificate", + description: + "Certificate for services requiring both server and client authentication capabilities", + useCase: "Microservices, service mesh, dual authentication", + formData: { + name: "Dual-Purpose Server Certificate", + description: + "Certificate for services requiring both server and client authentication capabilities", + keyUsages: { + requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE], + optionalUsages: [ + CertKeyUsageType.DIGITAL_SIGNATURE, + CertKeyUsageType.KEY_ENCIPHERMENT, + CertKeyUsageType.KEY_AGREEMENT + ] + }, + extendedKeyUsages: { + requiredUsages: [ + CertExtendedKeyUsageType.SERVER_AUTH, + CertExtendedKeyUsageType.CLIENT_AUTH + ], + optionalUsages: [CertExtendedKeyUsageType.SERVER_AUTH, CertExtendedKeyUsageType.CLIENT_AUTH] + }, + validity: { + maxDuration: { + value: 365, + unit: CertDurationUnit.DAYS + } + }, + attributes: [ + { + type: CertSubjectAttributeType.COMMON_NAME, + include: CertSubjectAttributeInclude.OPTIONAL, + value: ["*"] + } + ], + subjectAlternativeNames: [ + { + type: CertSubjectAlternativeNameType.DNS_NAME, + include: CertSanInclude.OPTIONAL, + value: ["*"] + }, + { + type: CertSubjectAlternativeNameType.IP_ADDRESS, + include: CertSanInclude.OPTIONAL, + value: ["*"] + } + ], + signatureAlgorithm: { + allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature] + }, + keyAlgorithm: { + allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key] + } + } + } +]; diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/components/SecretContainer.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/components/SecretContainer.tsx index 1b2e785fe..3193a1bf5 100644 --- a/frontend/src/pages/public/ViewSharedSecretByIDPage/components/SecretContainer.tsx +++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/components/SecretContainer.tsx @@ -13,6 +13,8 @@ import { Button, IconButton } from "@app/components/v2"; import { useTimedReset, useToggle } from "@app/hooks"; import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing"; +import { SecretShareInfo } from "./SecretShareInfo"; + type Props = { secret: TViewSharedSecretResponse["secret"]; secretKey: string | null; @@ -71,6 +73,7 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => { +