From 01d5774349013282e25e3ee8dc228c1040bcb74d Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 07:56:04 +0000 Subject: [PATCH 01/19] ignoring env files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c2065bc..1dbf6b6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### Env ### +.env* From 59a274567d8c8fe745174edcf2fcecb3e988fff6 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 07:56:20 +0000 Subject: [PATCH 02/19] adding application dev profile --- src/main/resources/application-dev.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/resources/application-dev.yaml diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..342d44e --- /dev/null +++ b/src/main/resources/application-dev.yaml @@ -0,0 +1,15 @@ +spring: + liquibase: + enabled: false + + sql: + init: + mode: always + + datasource: + initialization-mode: always + + jpa: + show-sql: true + hibernate: + ddl-auto: create-drop From ce69b4ff6c3bad036b55e67f78c67319c8fc96b6 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 10:02:46 +0000 Subject: [PATCH 03/19] adding dto --- .../dto/InfisicalWebhookEventResponse.java | 7 +++++++ .../com/abnov/infisicalbridge/dto/ProjectResponse.java | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/dto/InfisicalWebhookEventResponse.java create mode 100644 src/main/java/com/abnov/infisicalbridge/dto/ProjectResponse.java diff --git a/src/main/java/com/abnov/infisicalbridge/dto/InfisicalWebhookEventResponse.java b/src/main/java/com/abnov/infisicalbridge/dto/InfisicalWebhookEventResponse.java new file mode 100644 index 0000000..43d8e6d --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/dto/InfisicalWebhookEventResponse.java @@ -0,0 +1,7 @@ +package com.abnov.infisicalbridge.dto; + +public record InfisicalWebhookEventResponse( + String event, + ProjectResponse project, + long timestamp) { +} diff --git a/src/main/java/com/abnov/infisicalbridge/dto/ProjectResponse.java b/src/main/java/com/abnov/infisicalbridge/dto/ProjectResponse.java new file mode 100644 index 0000000..b2bb99a --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/dto/ProjectResponse.java @@ -0,0 +1,10 @@ +package com.abnov.infisicalbridge.dto; + +public record ProjectResponse( + String workspaceId, + String projectId, + String projectName, + String environment, + String secretPath) { + +} From 3774473eacde62ecf41150e47f9a0c01d3f886b2 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 10:03:03 +0000 Subject: [PATCH 04/19] adding signature verifier --- .../infisical/InfisicalSignatureVerifier.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/infisical/InfisicalSignatureVerifier.java diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalSignatureVerifier.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalSignatureVerifier.java new file mode 100644 index 0000000..1c17bb6 --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalSignatureVerifier.java @@ -0,0 +1,151 @@ +package com.abnov.infisicalbridge.infisical; + +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; + +/** + * Service to verify Infisical webhook signatures + */ +@Component +@Slf4j +public class InfisicalSignatureVerifier { + + private static final String HMAC_SHA256 = "HmacSHA256"; + private static final int DEFAULT_TOLERANCE_SECONDS = 300; // 5 minutes + + /** + * Verifies the signature of an Infisical webhook request + * + * @param payload The raw request body as string + * @param signatureHeader The value of x-infisical-signature header + * @param secretKey Your secret key from Infisical + * @return true if signature is valid, false otherwise + */ + public boolean verifySignature(String payload, String signatureHeader, String secretKey) { + return verifySignature(payload, signatureHeader, secretKey, DEFAULT_TOLERANCE_SECONDS); + } + + /** + * Verifies the signature with custom tolerance + * + * @param payload The raw request body as string + * @param signatureHeader The value of x-infisical-signature header (format: + * t=;) + * @param secretKey Your secret key from Infisical + * @param toleranceInSeconds Time tolerance for replay attacks + * @return true if signature is valid, false otherwise + */ + public boolean verifySignature(String payload, String signatureHeader, String secretKey, int toleranceInSeconds) { + try { + // Parse the signature header: t=; + String[] parts = signatureHeader.split(";"); + if (parts.length != 2) { + log.error("Invalid signature header format"); + return false; + } + + String timestamp = parts[0].substring(2); // Remove "t=" + String receivedSignature = parts[1]; + + // Check timestamp to prevent replay attacks + long webhookTime = Long.parseLong(timestamp); + long currentTime; + + // Detect if timestamp is in milliseconds or seconds + if (timestamp.length() > 10) { + // Timestamp is in milliseconds + currentTime = Instant.now().toEpochMilli(); + long toleranceInMillis = (long) toleranceInSeconds * 1000; + if (currentTime - webhookTime > toleranceInMillis) { + log.error("Webhook timestamp is too old"); + return false; + } + } else { + // Timestamp is in seconds + currentTime = Instant.now().getEpochSecond(); + if (currentTime - webhookTime > toleranceInSeconds) { + log.error("Webhook timestamp is too old"); + return false; + } + } + + // Try different signature formats that Infisical might use + + // Format 1: timestamp.payload (most common) + String signedPayload1 = timestamp + "." + payload; + String expectedSignature1 = generateHmacSHA256(signedPayload1, secretKey); + + if (MessageDigest.isEqual( + receivedSignature.getBytes(StandardCharsets.UTF_8), + expectedSignature1.getBytes(StandardCharsets.UTF_8))) { + return true; + } + + // Format 2: payload only + String expectedSignature2 = generateHmacSHA256(payload, secretKey); + + if (MessageDigest.isEqual( + receivedSignature.getBytes(StandardCharsets.UTF_8), + expectedSignature2.getBytes(StandardCharsets.UTF_8))) { + return true; + } + + // Format 3: t=timestamp.payload (with t= prefix) + String signedPayload3 = signatureHeader.split(";")[0] + "." + payload; + String expectedSignature3 = generateHmacSHA256(signedPayload3, secretKey); + + if (MessageDigest.isEqual( + receivedSignature.getBytes(StandardCharsets.UTF_8), + expectedSignature3.getBytes(StandardCharsets.UTF_8))) { + return true; + } + + // No format matched + log.error("Signature verification failed - no matching format found"); + return false; + + } catch (Exception e) { + log.error("Error verifying signature: {}" + e.getMessage()); + return false; + } + } + + /** + * Generates HMAC SHA256 signature + */ + private String generateHmacSHA256(String data, String key) + throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(HMAC_SHA256); + SecretKeySpec secretKeySpec = new SecretKeySpec( + key.getBytes(StandardCharsets.UTF_8), + HMAC_SHA256); + mac.init(secretKeySpec); + + byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hmacBytes); + } + + /** + * Converts byte array to hex string + */ + private String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } +} From 2f8a561ffea8fa224bf2c12db3a10cc551830445 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 10:03:30 +0000 Subject: [PATCH 05/19] adding webhook controller --- .../infisical/InfisicalWebhookController.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java new file mode 100644 index 0000000..a530a9f --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java @@ -0,0 +1,37 @@ +package com.abnov.infisicalbridge.infisical; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequestMapping("/webhook") +@RequiredArgsConstructor +@Slf4j +public class InfisicalWebhookController { + private final InfisicalSignatureVerifier signatureVerifier; + + @PostMapping + public void handleWebhook( + @RequestBody String payload, + @RequestHeader(value = "X-Infisical-Signature", required = false) String signature) { + // Check if signature header is present + if (signature == null || signature.isEmpty()) { + log.error("Missing signature header"); + return; + } + + // Verify the signature + if (!signatureVerifier.verifySignature(payload, signature, "demoa")) { + log.error("Invalid signature"); + return; + } + + log.info("Webhook received and verified: {}", payload); + } +} From 19cad9a450fd6c695d87293c398c79fa2b47ddf4 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 10:03:40 +0000 Subject: [PATCH 06/19] adding security config --- .../config/SecurityConfig.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/config/SecurityConfig.java diff --git a/src/main/java/com/abnov/infisicalbridge/config/SecurityConfig.java b/src/main/java/com/abnov/infisicalbridge/config/SecurityConfig.java new file mode 100644 index 0000000..46286ca --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/config/SecurityConfig.java @@ -0,0 +1,22 @@ +package com.abnov.infisicalbridge.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.disable()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/webhook/**").permitAll() + .anyRequest().authenticated()); + + return http.build(); + } +} From 935793812b8cbcb0c820f8183abadb643fe3390a Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 11:25:37 +0000 Subject: [PATCH 07/19] removing rabbitmq integration --- build.gradle.kts | 13 +++++++++---- compose.yaml | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index cb4f4ce..5b34fe5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,9 +27,11 @@ repositories { extra["springCloudVersion"] = "2025.1.0" extra["springModulithVersion"] = "2.0.1" +extra["infisicalVersion"] = "3.0.5" + dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") - implementation("org.springframework.boot:spring-boot-starter-amqp") + // implementation("org.springframework.boot:spring-boot-starter-amqp") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-liquibase") implementation("org.springframework.boot:spring-boot-starter-mail") @@ -48,12 +50,12 @@ dependencies { developmentOnly("org.springframework.boot:spring-boot-docker-compose") runtimeOnly("org.postgresql:postgresql") runtimeOnly("org.springframework.modulith:spring-modulith-actuator") - runtimeOnly("org.springframework.modulith:spring-modulith-events-amqp") + // runtimeOnly("org.springframework.modulith:spring-modulith-events-amqp") runtimeOnly("org.springframework.modulith:spring-modulith-observability") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") annotationProcessor("org.projectlombok:lombok") testImplementation("org.springframework.boot:spring-boot-starter-actuator-test") - testImplementation("org.springframework.boot:spring-boot-starter-amqp-test") + // testImplementation("org.springframework.boot:spring-boot-starter-amqp-test") testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test") testImplementation("org.springframework.boot:spring-boot-starter-liquibase-test") testImplementation("org.springframework.boot:spring-boot-starter-mail-test") @@ -67,8 +69,11 @@ dependencies { testImplementation("org.springframework.modulith:spring-modulith-starter-test") testImplementation("org.testcontainers:testcontainers-junit-jupiter") testImplementation("org.testcontainers:testcontainers-postgresql") - testImplementation("org.testcontainers:testcontainers-rabbitmq") + // testImplementation("org.testcontainers:testcontainers-rabbitmq") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + // https://mvnrepository.com/artifact/com.infisical/sdk + implementation("com.infisical:sdk:${property("infisicalVersion")}") } dependencyManagement { diff --git a/compose.yaml b/compose.yaml index 80e8ef1..8a4bd07 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,16 +1,16 @@ services: postgres: - image: 'postgres:latest' + image: "postgres:latest" environment: - - 'POSTGRES_DB=mydatabase' - - 'POSTGRES_PASSWORD=secret' - - 'POSTGRES_USER=myuser' + - "POSTGRES_DB=mydatabase" + - "POSTGRES_PASSWORD=secret" + - "POSTGRES_USER=myuser" ports: - - '5432' - rabbitmq: - image: 'rabbitmq:latest' - environment: - - 'RABBITMQ_DEFAULT_PASS=secret' - - 'RABBITMQ_DEFAULT_USER=myuser' - ports: - - '5672' + - "5432" + # rabbitmq: + # image: 'rabbitmq:latest' + # environment: + # - 'RABBITMQ_DEFAULT_PASS=secret' + # - 'RABBITMQ_DEFAULT_USER=myuser' + # ports: + # - '5672' From a63540a01b01e5149c2f9c77da52a991c6b670f5 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 11:26:17 +0000 Subject: [PATCH 08/19] adding infisical properties --- .../infisical/InfisicalProperties.java | 24 +++++++++++++++++++ src/main/resources/application.yaml | 5 ++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java new file mode 100644 index 0000000..033f0de --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java @@ -0,0 +1,24 @@ +package com.abnov.infisicalbridge.infisical; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotBlank; + +@Data +@Configuration +@ConfigurationProperties(prefix = "infisical") +@Validated +public class InfisicalProperties { + + @NotBlank(message = "Infisical API URL is required") + private String apiUrl; + + @NotBlank(message = "Infisical client ID is required") + private String clientId; + + @NotBlank(message = "Infisical client secret is required") + private String clientSecret; +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1c129d6..0a3c139 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,3 +1,8 @@ spring: application: name: infisical-bridge + +infisical: + api-url: ${INFISICAL_API_URL} + client-id: ${INFISICAL_CLIENT_ID} + client-secret: ${INFISICAL_CLIENT_SECRET} From bc9c2da3725b139065d4029cb6fabca7f180c14c Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 11:26:25 +0000 Subject: [PATCH 09/19] adding app config --- .../abnov/infisicalbridge/config/AppConfig.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/config/AppConfig.java diff --git a/src/main/java/com/abnov/infisicalbridge/config/AppConfig.java b/src/main/java/com/abnov/infisicalbridge/config/AppConfig.java new file mode 100644 index 0000000..c90c458 --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/config/AppConfig.java @@ -0,0 +1,15 @@ +package com.abnov.infisicalbridge.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@Configuration +public class AppConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} From cde10a4b3e084a5c65f930a7e181c7c014a4dc4f Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 13:47:34 +0000 Subject: [PATCH 10/19] adding dokploy properties --- .../dokploy/DokployProperties.java | 21 +++++++++++++++++++ src/main/resources/application.yaml | 4 ++++ 2 files changed, 25 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/dokploy/DokployProperties.java diff --git a/src/main/java/com/abnov/infisicalbridge/dokploy/DokployProperties.java b/src/main/java/com/abnov/infisicalbridge/dokploy/DokployProperties.java new file mode 100644 index 0000000..9fcafae --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/dokploy/DokployProperties.java @@ -0,0 +1,21 @@ +package com.abnov.infisicalbridge.dokploy; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotBlank; + +@Data +@Configuration +@ConfigurationProperties(prefix = "dokploy") +@Validated +public class DokployProperties { + + @NotBlank(message = "Dokploy API URL is required") + private String apiUrl; + + @NotBlank(message = "Dokploy API KEY is required") + private String apiKey; +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 0a3c139..43c27d0 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -6,3 +6,7 @@ infisical: api-url: ${INFISICAL_API_URL} client-id: ${INFISICAL_CLIENT_ID} client-secret: ${INFISICAL_CLIENT_SECRET} + +dokploy: + api-url: ${DOKPLOY_API_URL} + api-key: ${DOKPLOY_API_KEY} From dbdfea671ef89f0a2665a73889c32df66b9a0f6b Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 13:47:59 +0000 Subject: [PATCH 11/19] updating infisical properties --- .../abnov/infisicalbridge/infisical/InfisicalProperties.java | 3 +++ src/main/resources/application.yaml | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java index 033f0de..cdead2a 100644 --- a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalProperties.java @@ -21,4 +21,7 @@ public class InfisicalProperties { @NotBlank(message = "Infisical client secret is required") private String clientSecret; + + @NotBlank(message = "Infisical webhook secret is required") + private String webhookSecret; } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 43c27d0..71f1d9c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -6,6 +6,7 @@ infisical: api-url: ${INFISICAL_API_URL} client-id: ${INFISICAL_CLIENT_ID} client-secret: ${INFISICAL_CLIENT_SECRET} + webhook-secret: ${INFISICAL_WEBHOOK_SECRET} dokploy: api-url: ${DOKPLOY_API_URL} From 816177c2ec7a383e6debfb0f545e2e0f654a12a5 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 13:48:25 +0000 Subject: [PATCH 12/19] enabling feign clients --- .../abnov/infisicalbridge/InfisicalBridgeApplication.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/abnov/infisicalbridge/InfisicalBridgeApplication.java b/src/main/java/com/abnov/infisicalbridge/InfisicalBridgeApplication.java index c7c68c0..c4ab5fb 100644 --- a/src/main/java/com/abnov/infisicalbridge/InfisicalBridgeApplication.java +++ b/src/main/java/com/abnov/infisicalbridge/InfisicalBridgeApplication.java @@ -2,12 +2,14 @@ package com.abnov.infisicalbridge; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication +@EnableFeignClients public class InfisicalBridgeApplication { - public static void main(String[] args) { - SpringApplication.run(InfisicalBridgeApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(InfisicalBridgeApplication.class, args); + } } From 21d99f9923f6fd80b5d554e339a2b07731f6ef3e Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 13:49:50 +0000 Subject: [PATCH 13/19] adding dokploy client --- .../dokploy/DokployClient.java | 14 +++++++++++ .../dokploy/DokployFeignConfig.java | 24 +++++++++++++++++++ .../dto/DokployComposeUpdateRequest.java | 6 +++++ 3 files changed, 44 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/dokploy/DokployClient.java create mode 100644 src/main/java/com/abnov/infisicalbridge/dokploy/DokployFeignConfig.java create mode 100644 src/main/java/com/abnov/infisicalbridge/dto/DokployComposeUpdateRequest.java diff --git a/src/main/java/com/abnov/infisicalbridge/dokploy/DokployClient.java b/src/main/java/com/abnov/infisicalbridge/dokploy/DokployClient.java new file mode 100644 index 0000000..9a8b768 --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/dokploy/DokployClient.java @@ -0,0 +1,14 @@ +package com.abnov.infisicalbridge.dokploy; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest; + +@FeignClient(name = "dokployClient", url = "${dokploy.api-url}", configuration = DokployFeignConfig.class) +public interface DokployClient { + + @PostMapping("/compose.update") + void updateCompose(@RequestBody DokployComposeUpdateRequest request); +} diff --git a/src/main/java/com/abnov/infisicalbridge/dokploy/DokployFeignConfig.java b/src/main/java/com/abnov/infisicalbridge/dokploy/DokployFeignConfig.java new file mode 100644 index 0000000..6ff5f6c --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/dokploy/DokployFeignConfig.java @@ -0,0 +1,24 @@ +package com.abnov.infisicalbridge.dokploy; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import feign.RequestInterceptor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Configuration +@RequiredArgsConstructor +@Slf4j +public class DokployFeignConfig { + + private final DokployProperties properties; + + @Bean + public RequestInterceptor dokployRequestInterceptor() { + return requestTemplate -> { + // Add API key to every request + requestTemplate.header("x-api-key", properties.getApiKey()); + }; + } +} diff --git a/src/main/java/com/abnov/infisicalbridge/dto/DokployComposeUpdateRequest.java b/src/main/java/com/abnov/infisicalbridge/dto/DokployComposeUpdateRequest.java new file mode 100644 index 0000000..38eab79 --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/dto/DokployComposeUpdateRequest.java @@ -0,0 +1,6 @@ +package com.abnov.infisicalbridge.dto; + +public record DokployComposeUpdateRequest( + String composeId, + String env) { +} From 66965e0ff2252e6f12e54a67ebb75f1dedc6c6d9 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 13:50:56 +0000 Subject: [PATCH 14/19] adding infisical service --- .../infisical/InfisicalService.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/java/com/abnov/infisicalbridge/infisical/InfisicalService.java diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalService.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalService.java new file mode 100644 index 0000000..1e5a29d --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalService.java @@ -0,0 +1,45 @@ +package com.abnov.infisicalbridge.infisical; + +import org.springframework.stereotype.Service; + +import com.infisical.sdk.InfisicalSdk; +import com.infisical.sdk.config.SdkConfig; +import com.infisical.sdk.util.InfisicalException; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class InfisicalService { + private final InfisicalProperties properties; + + @Getter + private final InfisicalSdk sdk; + + public InfisicalService(InfisicalProperties properties) { + this.properties = properties; + this.sdk = initializeClient(); + } + + private InfisicalSdk initializeClient() { + try { + log.info("Initializing Infisical SDK"); + var sdkInstance = new InfisicalSdk( + new SdkConfig.Builder() + .withSiteUrl(properties.getApiUrl()) + .build()); + + sdkInstance.Auth().UniversalAuthLogin( + properties.getClientId(), + properties.getClientSecret()); + + log.info("Successfully authenticated with Infisical"); + return sdkInstance; + + } catch (InfisicalException e) { + log.error("Failed to initialize Infisical SDK", e); + throw new IllegalStateException("Failed to initialize Infisical SDK", e); + } + } +} From 72a8501d378f938fb1384c6e03fae6853ac14eef Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 13:51:09 +0000 Subject: [PATCH 15/19] updating webhook controller --- .../infisical/InfisicalWebhookController.java | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java index a530a9f..a810ca3 100644 --- a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java @@ -1,11 +1,27 @@ package com.abnov.infisicalbridge.infisical; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.abnov.infisicalbridge.dokploy.DokployClient; +import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest; +import com.abnov.infisicalbridge.dto.DokployComposeUpdateResponse; +import com.abnov.infisicalbridge.dto.InfisicalWebhookEventResponse; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.infisical.sdk.InfisicalSdk; +import com.infisical.sdk.models.Secret; +import com.infisical.sdk.util.InfisicalException; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -15,23 +31,64 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class InfisicalWebhookController { private final InfisicalSignatureVerifier signatureVerifier; + private final InfisicalService service; + private final InfisicalProperties infisicalProperties; + private final ObjectMapper objectMapper; + private final DokployClient dokployClient; @PostMapping - public void handleWebhook( + public ResponseEntity handleWebhook( @RequestBody String payload, - @RequestHeader(value = "X-Infisical-Signature", required = false) String signature) { - // Check if signature header is present + @RequestParam String dokployComposeId, + @RequestHeader(value = "X-Infisical-Signature", required = false) String signature) + throws InfisicalException { + if (signature == null || signature.isEmpty()) { - log.error("Missing signature header"); - return; + log.warn("Missing X-Infisical-Signature header"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - // Verify the signature - if (!signatureVerifier.verifySignature(payload, signature, "demoa")) { - log.error("Invalid signature"); - return; + if (!signatureVerifier.verifySignature(payload, signature, infisicalProperties.getWebhookSecret())) { + log.warn("Invalid webhook signature"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - log.info("Webhook received and verified: {}", payload); + InfisicalWebhookEventResponse webhookEvent; + try { + webhookEvent = objectMapper.readValue(payload, InfisicalWebhookEventResponse.class); + } catch (JsonProcessingException e) { + log.error("Invalid webhook payload", e); + return ResponseEntity.badRequest().build(); + } + + InfisicalSdk sdk = service.getSdk(); + List secrets = sdk.Secrets().ListSecrets( + webhookEvent.project().projectId(), + webhookEvent.project().environment(), + webhookEvent.project().secretPath(), + false, + false, + false); + + if (secrets.isEmpty()) { + log.warn("No secrets found for project={} env={}", + webhookEvent.project().projectName(), + webhookEvent.project().environment()); + return ResponseEntity.noContent().build(); + } + + String envContent = secrets.stream() + .map(s -> s.getSecretKey() + "=" + s.getSecretValue()) + .collect(Collectors.joining("\n")); + + try { + dokployClient.updateCompose( + new DokployComposeUpdateRequest(dokployComposeId, envContent)); + } catch (Exception e) { + log.error("Failed to update Dokploy compose {}", dokployComposeId, e); + return ResponseEntity.status(HttpStatus.BAD_GATEWAY).build(); + } + + return ResponseEntity.ok().build(); } } From d4491a6c053a6000d9cadf14647ad1bb45b439f9 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 13:59:54 +0000 Subject: [PATCH 16/19] remove unecessary code --- build.gradle.kts | 30 ------------------- .../infisical/InfisicalWebhookController.java | 1 - 2 files changed, 31 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5b34fe5..a523931 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,51 +25,22 @@ repositories { } extra["springCloudVersion"] = "2025.1.0" -extra["springModulithVersion"] = "2.0.1" extra["infisicalVersion"] = "3.0.5" dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") - // implementation("org.springframework.boot:spring-boot-starter-amqp") - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-liquibase") - implementation("org.springframework.boot:spring-boot-starter-mail") - implementation("org.springframework.boot:spring-boot-starter-quartz") - implementation("org.springframework.boot:spring-boot-starter-restclient") implementation("org.springframework.boot:spring-boot-starter-security") - implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-webmvc") implementation("org.springframework.cloud:spring-cloud-starter-openfeign") - implementation("org.springframework.modulith:spring-modulith-events-api") - implementation("org.springframework.modulith:spring-modulith-starter-core") - implementation("org.springframework.modulith:spring-modulith-starter-jpa") - implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6") compileOnly("org.projectlombok:lombok") - developmentOnly("org.springframework.boot:spring-boot-docker-compose") - runtimeOnly("org.postgresql:postgresql") - runtimeOnly("org.springframework.modulith:spring-modulith-actuator") - // runtimeOnly("org.springframework.modulith:spring-modulith-events-amqp") - runtimeOnly("org.springframework.modulith:spring-modulith-observability") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") annotationProcessor("org.projectlombok:lombok") testImplementation("org.springframework.boot:spring-boot-starter-actuator-test") - // testImplementation("org.springframework.boot:spring-boot-starter-amqp-test") - testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test") - testImplementation("org.springframework.boot:spring-boot-starter-liquibase-test") - testImplementation("org.springframework.boot:spring-boot-starter-mail-test") - testImplementation("org.springframework.boot:spring-boot-starter-quartz-test") - testImplementation("org.springframework.boot:spring-boot-starter-restclient-test") testImplementation("org.springframework.boot:spring-boot-starter-security-test") - testImplementation("org.springframework.boot:spring-boot-starter-thymeleaf-test") testImplementation("org.springframework.boot:spring-boot-starter-validation-test") testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") - testImplementation("org.springframework.boot:spring-boot-testcontainers") - testImplementation("org.springframework.modulith:spring-modulith-starter-test") - testImplementation("org.testcontainers:testcontainers-junit-jupiter") - testImplementation("org.testcontainers:testcontainers-postgresql") - // testImplementation("org.testcontainers:testcontainers-rabbitmq") testRuntimeOnly("org.junit.platform:junit-platform-launcher") // https://mvnrepository.com/artifact/com.infisical/sdk @@ -78,7 +49,6 @@ dependencies { dependencyManagement { imports { - mavenBom("org.springframework.modulith:spring-modulith-bom:${property("springModulithVersion")}") mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") } } diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java index a810ca3..b47b6bb 100644 --- a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalWebhookController.java @@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.RestController; import com.abnov.infisicalbridge.dokploy.DokployClient; import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest; -import com.abnov.infisicalbridge.dto.DokployComposeUpdateResponse; import com.abnov.infisicalbridge.dto.InfisicalWebhookEventResponse; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; From ab0f9ceb507fec4e154197b1280d1094219192f2 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 14:01:48 +0000 Subject: [PATCH 17/19] remove application-dev yaml --- src/main/resources/application-dev.yaml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 src/main/resources/application-dev.yaml diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml deleted file mode 100644 index 342d44e..0000000 --- a/src/main/resources/application-dev.yaml +++ /dev/null @@ -1,15 +0,0 @@ -spring: - liquibase: - enabled: false - - sql: - init: - mode: always - - datasource: - initialization-mode: always - - jpa: - show-sql: true - hibernate: - ddl-auto: create-drop From 668ebfcc848169912b5bb8a8ec8d01d3bf3e7741 Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 14:12:03 +0000 Subject: [PATCH 18/19] adding ci --- Dockerfile | 19 +++++++++++++++++++ docker-compose.yml | 11 +++++++++++ 2 files changed, 30 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a9d5377 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM gradle:8.10.2-jdk17 AS build +WORKDIR /app + +COPY build.gradle.kts settings.gradle.kts gradlew ./ +COPY gradle ./gradle + +RUN ./gradlew dependencies --no-daemon || true + +COPY . . +RUN ./gradlew clean bootJar --no-daemon + +FROM eclipse-temurin:17-jdk-alpine +WORKDIR /app + +COPY --from=build /app/build/libs/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c33a3d7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + infisical-bridge: + build: . + restart: always + environment: + INFISICAL_API_URL: ${INFISICAL_API_URL} + INFISICAL_CLIENT_ID: ${INFISICAL_CLIENT_ID} + INFISICAL_CLIENT_SECRET: ${INFISICAL_CLIENT_SECRET} + INFISICAL_WEBHOOK_SECRET: ${INFISICAL_WEBHOOK_SECRET} + DOKPLOY_API_URL: ${DOKPLOY_API_URL} + DOKPLOY_API_KEY: ${DOKPLOY_API_KEY} From 12263fe0cb07b087a68a21ac749bc093c02526de Mon Sep 17 00:00:00 2001 From: Aboubacar TRAORE Date: Wed, 24 Dec 2025 14:21:24 +0000 Subject: [PATCH 19/19] use java 21 instead of 17 --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a9d5377..5ccc76f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:8.10.2-jdk17 AS build +FROM gradle:8.10.2-jdk21 AS build WORKDIR /app COPY build.gradle.kts settings.gradle.kts gradlew ./ @@ -9,7 +9,7 @@ RUN ./gradlew dependencies --no-daemon || true COPY . . RUN ./gradlew clean bootJar --no-daemon -FROM eclipse-temurin:17-jdk-alpine +FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY --from=build /app/build/libs/*.jar app.jar @@ -17,3 +17,4 @@ COPY --from=build /app/build/libs/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] +