Compare commits

...

33 Commits

Author SHA1 Message Date
1b1c6e54b1 Merge pull request 'update doc' (#9) from dev into main
Reviewed-on: #9
2025-12-25 07:56:44 +00:00
c4568a2f23 update doc 2025-12-25 07:56:03 +00:00
6000582d55 Merge pull request 'adding support for dokploy application' (#8) from dev into main
Reviewed-on: #8
2025-12-25 07:40:07 +00:00
0a2937ad55 adding support for dokploy application 2025-12-25 07:39:17 +00:00
22a244832e Merge pull request 'update doc' (#7) from dev into main
Reviewed-on: #7
2025-12-24 16:51:13 +00:00
ef2b9667fc update doc 2025-12-24 16:50:46 +00:00
c98900a9ff Merge pull request 'update doc' (#6) from dev into main
Reviewed-on: #6
2025-12-24 16:49:21 +00:00
1684099f74 update doc 2025-12-24 16:48:43 +00:00
32cb5b57d7 Merge pull request 'update README' (#5) from dev into main
Reviewed-on: #5
2025-12-24 16:45:40 +00:00
a65b2dbeb0 update README 2025-12-24 16:44:54 +00:00
6037788dbd Merge pull request 'add README.md' (#4) from dev into main
Reviewed-on: #4
2025-12-24 16:40:33 +00:00
aef24c809b add README.md 2025-12-24 16:39:16 +00:00
706631c57b Merge pull request 'dev' (#3) from dev into main
Reviewed-on: #3
2025-12-24 15:05:01 +00:00
1ee7979091 Merge pull request 'ci' (#2) from ci into dev
Reviewed-on: #2
2025-12-24 15:04:04 +00:00
12263fe0cb use java 21 instead of 17 2025-12-24 14:21:24 +00:00
668ebfcc84 adding ci 2025-12-24 14:12:03 +00:00
ab0f9ceb50 remove application-dev yaml 2025-12-24 14:01:48 +00:00
d4491a6c05 remove unecessary code 2025-12-24 13:59:54 +00:00
72a8501d37 updating webhook controller 2025-12-24 13:51:09 +00:00
66965e0ff2 adding infisical service 2025-12-24 13:50:56 +00:00
21d99f9923 adding dokploy client 2025-12-24 13:49:50 +00:00
816177c2ec enabling feign clients 2025-12-24 13:48:25 +00:00
dbdfea671e updating infisical properties 2025-12-24 13:47:59 +00:00
cde10a4b3e adding dokploy properties 2025-12-24 13:47:34 +00:00
bc9c2da372 adding app config 2025-12-24 11:26:25 +00:00
a63540a01b adding infisical properties 2025-12-24 11:26:17 +00:00
935793812b removing rabbitmq integration 2025-12-24 11:25:37 +00:00
19cad9a450 adding security config 2025-12-24 10:03:40 +00:00
2f8a561ffe adding webhook controller 2025-12-24 10:03:30 +00:00
3774473eac adding signature verifier 2025-12-24 10:03:03 +00:00
ce69b4ff6c adding dto 2025-12-24 10:02:46 +00:00
59a274567d adding application dev profile 2025-12-24 07:56:20 +00:00
01d5774349 ignoring env files 2025-12-24 07:56:04 +00:00
21 changed files with 668 additions and 45 deletions

3
.gitignore vendored
View File

@@ -35,3 +35,6 @@ out/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
### Env ###
.env*

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM gradle:8.10.2-jdk21 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:21-jre-alpine
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

143
README.md Normal file
View File

@@ -0,0 +1,143 @@
# Infisical ↔ Dokploy Bridge
A Spring Boot (Java 21) application acting as a secure bridge between Infisical and Dokploy, enabling automated synchronization and deployment of secrets through APIs and webhooks.
## Features
- Secure integration with Infisical
- Automated updates via Dokploy API
- Webhook-driven synchronization
- Docker and Docker Compose ready
## Architecture Overview
Infisical
↓ (Webhook / API)
InfisicalDokploy Bridge (Spring Boot)
↓ (Dokploy API)
Dokploy
## Requirements
- Java 21
- Docker and Docker Compose
- Infisical account
- Dokploy instance with API access
## Environment Variables
### Infisical
- INFISICAL_API_URL: Base URL of Infisical API
- INFISICAL_CLIENT_ID: Infisical service client ID
- INFISICAL_CLIENT_SECRET: Infisical service client secret
- INFISICAL_WEBHOOK_SECRET: Webhook signature validation secret
### Dokploy
- DOKPLOY_API_URL: Base URL of Dokploy API
- DOKPLOY_API_KEY: Dokploy API key
## Docker Compose
```txt
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}
```
## Running
With Docker Compose:
```sh
docker compose up -d --build
```
Local development:
```sh
./gradlew bootRun
```
Application runs on http://localhost:8080
Use a service like ngrok.
## Infisical Webhook Configuration
When creating a webhook in Infisical, the following rules must be respected.
### Webhook URL Formats
Infisical bridge supports two webhook URL formats, depending on the Dokploy resource you want to update.
#### Dokploy Compose Webhook
`${INFISICAL_API_URL}/webhook?dokployComposeId=${DOKPLOY_COMPOSE_ID}`
Parameters:
- dokployComposeId (required):
The identifier of the target Dokploy Compose.
This value is used to determine which Dokploy compose service should be updated when the webhook is triggered.
#### Dokploy Application Webhook
`${INFISICAL_API_URL}/webhook?dokployApplicationId=${DOKPLOY_APPLICATION_ID}`
Parameters:
- dokployApplicationId (required):
The identifier of the target Dokploy Application.
This value is used to determine which Dokploy application should be updated when the webhook is triggered.
#### Notes
- Exactly one identifier must be provided per webhook URL.
- If no identifier or multiple identifiers are provided, the webhook request will be rejected.
- Ensure the provided ID matches an existing Dokploy resource.
### Webhook Secret
The webhook secret **must exactly match**:
`${INFISICAL_WEBHOOK_SECRET}`
Requests with an invalid or missing secret will be rejected.
## Webhooks Behavior
- Incoming webhook signatures are validated
- Secrets are fetched from Infisical
- Dokploy is updated using its API
- Invalid or unsigned requests are ignored
## Security Notes
- Secrets are never persisted
- Configuration is environment-driven
- HTTPS is recommended in production
- Restrict network access to trusted sources only
## Testing
```sh
./gradlew test
```
## Tech Stack
- Java 21
- Spring Boot
- Gradle (Kotlin DSL)
- Docker / Docker Compose
## License
MIT License

View File

@@ -25,55 +25,30 @@ repositories {
} }
extra["springCloudVersion"] = "2025.1.0" extra["springCloudVersion"] = "2025.1.0"
extra["springModulithVersion"] = "2.0.1"
extra["infisicalVersion"] = "3.0.5"
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator") 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-security")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-webmvc") implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign") 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") 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.springframework.boot:spring-boot-configuration-processor")
annotationProcessor("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test") 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-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-validation-test")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-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") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
// https://mvnrepository.com/artifact/com.infisical/sdk
implementation("com.infisical:sdk:${property("infisicalVersion")}")
} }
dependencyManagement { dependencyManagement {
imports { imports {
mavenBom("org.springframework.modulith:spring-modulith-bom:${property("springModulithVersion")}")
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
} }
} }

View File

@@ -1,16 +1,16 @@
services: services:
postgres: postgres:
image: 'postgres:latest' image: "postgres:latest"
environment: environment:
- 'POSTGRES_DB=mydatabase' - "POSTGRES_DB=mydatabase"
- 'POSTGRES_PASSWORD=secret' - "POSTGRES_PASSWORD=secret"
- 'POSTGRES_USER=myuser' - "POSTGRES_USER=myuser"
ports: ports:
- '5432' - "5432"
rabbitmq: # rabbitmq:
image: 'rabbitmq:latest' # image: 'rabbitmq:latest'
environment: # environment:
- 'RABBITMQ_DEFAULT_PASS=secret' # - 'RABBITMQ_DEFAULT_PASS=secret'
- 'RABBITMQ_DEFAULT_USER=myuser' # - 'RABBITMQ_DEFAULT_USER=myuser'
ports: # ports:
- '5672' # - '5672'

11
docker-compose.yml Normal file
View File

@@ -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}

View File

@@ -2,12 +2,14 @@ package com.abnov.infisicalbridge;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication @SpringBootApplication
@EnableFeignClients
public class InfisicalBridgeApplication { public class InfisicalBridgeApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(InfisicalBridgeApplication.class, args); SpringApplication.run(InfisicalBridgeApplication.class, args);
} }
} }

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,18 @@
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.DokployApplicationUpdateRequest;
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);
@PostMapping("/application.update")
void updateApplication(@RequestBody DokployApplicationUpdateRequest request);
}

View File

@@ -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());
};
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,6 @@
package com.abnov.infisicalbridge.dto;
public record DokployApplicationUpdateRequest(
String applicationId,
String env) {
}

View File

@@ -0,0 +1,6 @@
package com.abnov.infisicalbridge.dto;
public record DokployComposeUpdateRequest(
String composeId,
String env) {
}

View File

@@ -0,0 +1,7 @@
package com.abnov.infisicalbridge.dto;
public record InfisicalWebhookEventResponse(
String event,
ProjectResponse project,
long timestamp) {
}

View File

@@ -0,0 +1,10 @@
package com.abnov.infisicalbridge.dto;
public record ProjectResponse(
String workspaceId,
String projectId,
String projectName,
String environment,
String secretPath) {
}

View File

@@ -0,0 +1,27 @@
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;
@NotBlank(message = "Infisical webhook secret is required")
private String webhookSecret;
}

View File

@@ -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);
}
}
}

View File

@@ -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=<timestamp>;<signature>)
* @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=<timestamp>;<signature>
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();
}
}

View File

@@ -0,0 +1,107 @@
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.DokployApplicationUpdateRequest;
import com.abnov.infisicalbridge.dto.DokployComposeUpdateRequest;
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;
@RestController
@RequestMapping("/webhook")
@RequiredArgsConstructor
@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 ResponseEntity<Void> handleWebhook(
@RequestBody String payload,
@RequestParam(required = false) String dokployComposeId,
@RequestParam(required = false) String dokployApplicationId,
@RequestHeader(value = "X-Infisical-Signature", required = false) String signature)
throws InfisicalException {
if (signature == null || signature.isEmpty()) {
log.warn("Missing X-Infisical-Signature header");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
if (!signatureVerifier.verifySignature(payload, signature, infisicalProperties.getWebhookSecret())) {
log.warn("Invalid webhook signature");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
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<Secret> 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"));
if (dokployComposeId != null) {
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();
}
}
if (dokployApplicationId != null) {
try {
dokployClient.updateApplication(
new DokployApplicationUpdateRequest(dokployApplicationId, envContent));
} catch (Exception e) {
log.error("Failed to update Dokploy application {}", dokployApplicationId, e);
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).build();
}
}
return ResponseEntity.ok().build();
}
}

View File

@@ -1,3 +1,13 @@
spring: spring:
application: application:
name: infisical-bridge name: infisical-bridge
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}
api-key: ${DOKPLOY_API_KEY}