Certbot Integration in Java: Comprehensive SSL/TLS Certificate Management

Certbot is a widely-used tool for automatically managing Let's Encrypt SSL certificates. This guide covers Java integration for certificate automation, renewal, and deployment.


Core Concepts

What is Certbot?

  • Automated client for Let's Encrypt certificates
  • Manages SSL/TLS certificate issuance and renewal
  • Supports various web servers and deployment scenarios
  • Free SSL certificates with automatic renewal

Key Benefits:

  • Automated Certificate Management: Zero-touch certificate renewal
  • Cost-Effective: Free SSL certificates from Let's Encrypt
  • Security: Regular certificate rotation and updates
  • Integration: Works with various web servers and platforms

Architecture Overview

Java Application
↓
Certbot Manager
↓
Certbot CLI / API
↓
Let's Encrypt CA
↓
SSL/TLS Certificates
↓
Web Server / Application

Dependencies and Setup

Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<apache-commons.version>1.15</apache-commons.version>
<bouncycastle.version>1.76</bouncycastle.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>${apache-commons.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<!-- Bouncy Castle for Certificate Handling -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Monitoring -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.11.5</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Application Configuration
# application.yml
app:
certbot:
enabled: true
executable-path: /usr/bin/certbot
config-dir: /etc/letsencrypt
work-dir: /var/lib/letsencrypt
logs-dir: /var/log/letsencrypt
email: [email protected]
server: https://acme-v02.api.letsencrypt.org/directory  # Production
# server: https://acme-staging-v02.api.letsencrypt.org/directory  # Staging
rsa-key-size: 2048
renew-before-expiry: 30  # days
auto-renew: true
webroot-path: /var/www/html
domains:
- example.com
- www.example.com
deployment:
type: java  # java, nginx, apache
keystore-path: /etc/ssl/certs/keystore.p12
keystore-password: changeit
key-alias: tomcat
management:
endpoints:
web:
exposure:
include: health,info,metrics,certificates
endpoint:
certificates:
enabled: true
server:
ssl:
enabled: true
key-store: /etc/ssl/certs/keystore.p12
key-store-password: changeit
key-store-type: PKCS12
key-alias: tomcat
protocol: TLS
enabled-protocols: TLSv1.2,TLSv1.3
logging:
level:
com.example.certbot: DEBUG

Core Implementation

1. Configuration Classes
@Configuration
@ConfigurationProperties(prefix = "app.certbot")
@Data
@Validated
public class CertbotConfig {
private boolean enabled = true;
@NotBlank
private String executablePath = "/usr/bin/certbot";
@NotBlank
private String configDir = "/etc/letsencrypt";
@NotBlank
private String workDir = "/var/lib/letsencrypt";
@NotBlank
private String logsDir = "/var/log/letsencrypt";
@Email
@NotBlank
private String email;
@NotBlank
private String server = "https://acme-v02.api.letsencrypt.org/directory";
private int rsaKeySize = 2048;
private int renewBeforeExpiry = 30; // days
private boolean autoRenew = true;
private String webrootPath = "/var/www/html";
@NotEmpty
private List<String> domains = new ArrayList<>();
private DeploymentConfig deployment = new DeploymentConfig();
@Data
public static class DeploymentConfig {
private DeploymentType type = DeploymentType.JAVA;
private String keystorePath = "/etc/ssl/certs/keystore.p12";
private String keystorePassword = "changeit";
private String keyAlias = "tomcat";
private String certificatePath = "/etc/letsencrypt/live/{domain}/fullchain.pem";
private String privateKeyPath = "/etc/letsencrypt/live/{domain}/privkey.pem";
}
public enum DeploymentType {
JAVA, NGINX, APACHE, DOCKER
}
}
2. Certificate Model
@Data
public class CertificateInfo {
private String domain;
private File certificateFile;
private File privateKeyFile;
private File fullChainFile;
private X509Certificate certificate;
private PrivateKey privateKey;
private Instant expiryDate;
private Instant issueDate;
private String serialNumber;
private String issuer;
private CertificateStatus status;
public boolean isExpired() {
return expiryDate != null && expiryDate.isBefore(Instant.now());
}
public boolean expiresSoon() {
return expiryDate != null && 
expiryDate.isBefore(Instant.now().plus(30, ChronoUnit.DAYS));
}
public long getDaysUntilExpiry() {
if (expiryDate == null) return -1;
return ChronoUnit.DAYS.between(Instant.now(), expiryDate);
}
}
@Data
public class CertificateRequest {
@NotBlank
private String domain;
private List<String> additionalDomains = new ArrayList<>();
@Email
@NotBlank
private String email;
private boolean testMode = false;
private boolean forceRenewal = false;
private AuthenticationMethod authentication = AuthenticationMethod.WEBROOT;
private String webrootPath = "/var/www/html";
public List<String> getAllDomains() {
List<String> allDomains = new ArrayList<>();
allDomains.add(domain);
allDomains.addAll(additionalDomains);
return allDomains;
}
}
@Data
public class CertificateRenewalResult {
private final boolean success;
private final String message;
private final List<String> renewedDomains;
private final List<String> failedDomains;
private final Instant renewalTime;
private final Map<String, String> details;
public CertificateRenewalResult(boolean success, String message) {
this(success, message, Collections.emptyList(), Collections.emptyList(), 
Instant.now(), Collections.emptyMap());
}
}
public enum CertificateStatus {
VALID, EXPIRING_SOON, EXPIRED, REVOKED, UNKNOWN
}
public enum AuthenticationMethod {
WEBROOT, STANDALONE, DNS, MANUAL
}
3. Certbot Service Interface
public interface CertbotService {
// Certificate Operations
CertificateResult requestCertificate(CertificateRequest request) throws CertbotException;
CertificateRenewalResult renewCertificates(boolean force) throws CertbotException;
CertificateRevocationResult revokeCertificate(String domain, RevocationReason reason) throws CertbotException;
// Certificate Information
List<CertificateInfo> listCertificates() throws CertbotException;
Optional<CertificateInfo> getCertificateInfo(String domain) throws CertbotException;
List<CertificateInfo> getExpiringCertificates(int daysThreshold) throws CertbotException;
// Deployment
DeploymentResult deployCertificate(String domain) throws CertbotException;
DeploymentResult deployAllCertificates() throws CertbotException;
// Management
boolean isCertificateExpired(String domain) throws CertbotException;
boolean isCertificateExpiringSoon(String domain, int days) throws CertbotException;
void validateConfiguration() throws CertbotException;
// Auto-renewal
void setupAutoRenewal() throws CertbotException;
void disableAutoRenewal() throws CertbotException;
}
public class CertificateResult {
private final boolean success;
private final String message;
private final CertificateInfo certificateInfo;
private final List<String> challenges;
private final Instant issueTime;
// Constructors, getters
}
public class CertificateRevocationResult {
private final boolean success;
private final String message;
private final String domain;
private final RevocationReason reason;
private final Instant revocationTime;
// Constructors, getters
}
public enum RevocationReason {
UNSPECIFIED, KEY_COMPROMISE, CA_COMPROMISE, AFFILIATION_CHANGED,
SUPERSEDED, CESSATION_OF_OPERATION, CERTIFICATE_HOLD, REMOVE_FROM_CRL,
PRIVILEGE_WITHDRAWN, AA_COMPROMISE
}
public class DeploymentResult {
private final boolean success;
private final String message;
private final String domain;
private final DeploymentType deploymentType;
private final Instant deploymentTime;
private final Map<String, Object> details;
// Constructors, getters
}
4. Certbot Service Implementation
@Service
@Slf4j
public class CertbotServiceImpl implements CertbotService {
private final CertbotConfig certbotConfig;
private final ObjectMapper objectMapper;
private final MeterRegistry meterRegistry;
private final ScheduledExecutorService renewalExecutor;
private final long renewalCheckInterval = 86400000; // 24 hours
public CertbotServiceImpl(CertbotConfig certbotConfig,
ObjectMapper objectMapper,
MeterRegistry meterRegistry) {
this.certbotConfig = certbotConfig;
this.objectMapper = objectMapper;
this.meterRegistry = meterRegistry;
this.renewalExecutor = Executors.newSingleThreadScheduledExecutor(
r -> new Thread(r, "certbot-renewal-checker"));
initializeService();
}
@PostConstruct
public void initializeService() {
if (!certbotConfig.isEnabled()) {
log.info("Certbot service is disabled");
return;
}
try {
validateConfiguration();
setupAutoRenewal();
log.info("Certbot service initialized successfully");
} catch (Exception e) {
log.error("Failed to initialize Certbot service", e);
}
}
@Override
public CertificateResult requestCertificate(CertificateRequest request) throws CertbotException {
log.info("Requesting certificate for domain: {}", request.getDomain());
try {
validateCertificateRequest(request);
List<String> command = buildCertbotCommand(request);
ExecutionResult executionResult = executeCertbotCommand(command);
if (executionResult.isSuccess()) {
CertificateInfo certificateInfo = loadCertificateInfo(request.getDomain());
meterRegistry.counter("certbot.certificate.issued",
"domain", request.getDomain(),
"test_mode", String.valueOf(request.isTestMode())).increment();
log.info("Certificate successfully issued for domain: {}", request.getDomain());
return new CertificateResult(true, "Certificate issued successfully", 
certificateInfo, Collections.emptyList(), Instant.now());
} else {
meterRegistry.counter("certbot.certificate.failed",
"domain", request.getDomain(),
"error", executionResult.getErrorOutput()).increment();
throw new CertbotException("Certificate request failed: " + executionResult.getErrorOutput());
}
} catch (Exception e) {
log.error("Certificate request failed for domain: {}", request.getDomain(), e);
throw new CertbotException("Certificate request failed", e);
}
}
@Override
public CertificateRenewalResult renewCertificates(boolean force) throws CertbotException {
log.info("Renewing certificates (force: {})", force);
List<String> renewedDomains = new ArrayList<>();
List<String> failedDomains = new ArrayList<>();
Map<String, String> details = new HashMap<>();
try {
List<String> command = new ArrayList<>();
command.add(certbotConfig.getExecutablePath());
command.add("renew");
if (force) {
command.add("--force-renewal");
}
command.add("--non-interactive");
command.add("--agree-tos");
if (certbotConfig.isAutoRenew()) {
command.add("--deploy-hook");
command.add(buildDeployHookCommand());
}
ExecutionResult result = executeCertbotCommand(command);
if (result.isSuccess()) {
// Parse output to determine which certificates were renewed
renewedDomains = parseRenewedDomains(result.getOutput());
log.info("Successfully renewed {} certificates: {}", renewedDomains.size(), renewedDomains);
} else {
failedDomains = parseFailedDomains(result.getErrorOutput());
log.warn("Certificate renewal completed with errors. Failed domains: {}", failedDomains);
}
details.put("output", result.getOutput());
details.put("errorOutput", result.getErrorOutput());
details.put("exitCode", String.valueOf(result.getExitCode()));
meterRegistry.counter("certbot.renewal.attempted").increment();
if (!renewedDomains.isEmpty()) {
meterRegistry.counter("certbot.renewal.successful",
"renewed_count", String.valueOf(renewedDomains.size())).increment();
}
if (!failedDomains.isEmpty()) {
meterRegistry.counter("certbot.renewal.failed",
"failed_count", String.valueOf(failedDomains.size())).increment();
}
boolean overallSuccess = failedDomains.isEmpty() || !renewedDomains.isEmpty();
String message = String.format("Renewal completed. Success: %d, Failed: %d", 
renewedDomains.size(), failedDomains.size());
return new CertificateRenewalResult(overallSuccess, message, 
renewedDomains, failedDomains, Instant.now(), details);
} catch (Exception e) {
log.error("Certificate renewal failed", e);
meterRegistry.counter("certbot.renewal.error").increment();
throw new CertbotException("Certificate renewal failed", e);
}
}
@Override
public CertificateRevocationResult revokeCertificate(String domain, RevocationReason reason) throws CertbotException {
log.info("Revoking certificate for domain: {} with reason: {}", domain, reason);
try {
List<String> command = new ArrayList<>();
command.add(certbotConfig.getExecutablePath());
command.add("revoke");
command.add("--non-interactive");
command.add("--agree-tos");
command.add("--reason");
command.add(reason.name().toLowerCase());
command.add("--cert-name");
command.add(domain);
ExecutionResult result = executeCertbotCommand(command);
if (result.isSuccess()) {
log.info("Certificate revoked successfully for domain: {}", domain);
meterRegistry.counter("certbot.certificate.revoked",
"domain", domain, "reason", reason.name()).increment();
return new CertificateRevocationResult(true, "Certificate revoked successfully", 
domain, reason, Instant.now());
} else {
log.error("Certificate revocation failed for domain: {}", domain);
throw new CertbotException("Certificate revocation failed: " + result.getErrorOutput());
}
} catch (Exception e) {
log.error("Certificate revocation failed for domain: {}", domain, e);
throw new CertbotException("Certificate revocation failed", e);
}
}
@Override
public List<CertificateInfo> listCertificates() throws CertbotException {
try {
List<String> command = Arrays.asList(
certbotConfig.getExecutablePath(),
"certificates",
"--format", "json"
);
ExecutionResult result = executeCertbotCommand(command);
if (result.isSuccess()) {
return parseCertificatesJson(result.getOutput());
} else {
throw new CertbotException("Failed to list certificates: " + result.getErrorOutput());
}
} catch (Exception e) {
throw new CertbotException("Failed to list certificates", e);
}
}
@Override
public Optional<CertificateInfo> getCertificateInfo(String domain) throws CertbotException {
return listCertificates().stream()
.filter(cert -> cert.getDomain().equals(domain))
.findFirst();
}
@Override
public List<CertificateInfo> getExpiringCertificates(int daysThreshold) throws CertbotException {
Instant thresholdDate = Instant.now().plus(daysThreshold, ChronoUnit.DAYS);
return listCertificates().stream()
.filter(cert -> cert.getExpiryDate() != null && 
cert.getExpiryDate().isBefore(thresholdDate))
.collect(Collectors.toList());
}
@Override
public DeploymentResult deployCertificate(String domain) throws CertbotException {
log.info("Deploying certificate for domain: {}", domain);
try {
Optional<CertificateInfo> certificateInfo = getCertificateInfo(domain);
if (certificateInfo.isEmpty()) {
throw new CertbotException("Certificate not found for domain: " + domain);
}
switch (certbotConfig.getDeployment().getType()) {
case JAVA:
return deployToJavaKeystore(certificateInfo.get());
case NGINX:
return deployToNginx(certificateInfo.get());
case APACHE:
return deployToApache(certificateInfo.get());
case DOCKER:
return deployToDocker(certificateInfo.get());
default:
throw new CertbotException("Unsupported deployment type: " + 
certbotConfig.getDeployment().getType());
}
} catch (Exception e) {
log.error("Certificate deployment failed for domain: {}", domain, e);
throw new CertbotException("Certificate deployment failed", e);
}
}
@Override
public DeploymentResult deployAllCertificates() throws CertbotException {
log.info("Deploying all certificates");
List<String> deployedDomains = new ArrayList<>();
List<String> failedDomains = new ArrayList<>();
List<CertificateInfo> certificates = listCertificates();
for (CertificateInfo cert : certificates) {
try {
DeploymentResult result = deployCertificate(cert.getDomain());
if (result.isSuccess()) {
deployedDomains.add(cert.getDomain());
} else {
failedDomains.add(cert.getDomain());
}
} catch (Exception e) {
failedDomains.add(cert.getDomain());
log.error("Failed to deploy certificate for domain: {}", cert.getDomain(), e);
}
}
String message = String.format("Deployment completed. Success: %d, Failed: %d", 
deployedDomains.size(), failedDomains.size());
Map<String, Object> details = new HashMap<>();
details.put("deployed_domains", deployedDomains);
details.put("failed_domains", failedDomains);
return new DeploymentResult(failedDomains.isEmpty(), message, 
"all", certbotConfig.getDeployment().getType(), Instant.now(), details);
}
@Override
public boolean isCertificateExpired(String domain) throws CertbotException {
Optional<CertificateInfo> cert = getCertificateInfo(domain);
return cert.map(CertificateInfo::isExpired).orElse(true);
}
@Override
public boolean isCertificateExpiringSoon(String domain, int days) throws CertbotException {
Optional<CertificateInfo> cert = getCertificateInfo(domain);
return cert.map(c -> c.getDaysUntilExpiry() <= days).orElse(true);
}
@Override
public void validateConfiguration() throws CertbotException {
// Check if certbot executable exists and is executable
File certbotExecutable = new File(certbotConfig.getExecutablePath());
if (!certbotExecutable.exists()) {
throw new CertbotException("Certbot executable not found: " + certbotConfig.getExecutablePath());
}
if (!certbotExecutable.canExecute()) {
throw new CertbotException("Certbot executable is not executable: " + certbotConfig.getExecutablePath());
}
// Validate directories
validateDirectory(certbotConfig.getConfigDir(), "Config directory");
validateDirectory(certbotConfig.getWorkDir(), "Work directory");
validateDirectory(certbotConfig.getLogsDir(), "Logs directory");
// Test certbot version
try {
List<String> command = Arrays.asList(certbotConfig.getExecutablePath(), "--version");
ExecutionResult result = executeCertbotCommand(command);
if (!result.isSuccess()) {
throw new CertbotException("Failed to get certbot version");
}
log.info("Certbot version: {}", result.getOutput().trim());
} catch (Exception e) {
throw new CertbotException("Certbot version check failed", e);
}
}
@Override
public void setupAutoRenewal() throws CertbotException {
if (!certbotConfig.isAutoRenew()) {
log.info("Auto-renewal is disabled in configuration");
return;
}
renewalExecutor.scheduleAtFixedRate(() -> {
try {
log.info("Checking for certificate renewal...");
List<CertificateInfo> expiringCerts = getExpiringCertificates(certbotConfig.getRenewBeforeExpiry());
if (!expiringCerts.isEmpty()) {
log.info("Found {} certificates expiring within {} days. Starting renewal...", 
expiringCerts.size(), certbotConfig.getRenewBeforeExpiry());
CertificateRenewalResult result = renewCertificates(false);
if (result.isSuccess()) {
log.info("Auto-renewal completed successfully. Renewed: {}", result.getRenewedDomains());
} else {
log.warn("Auto-renewal completed with issues: {}", result.getMessage());
}
} else {
log.debug("No certificates require renewal at this time");
}
} catch (Exception e) {
log.error("Auto-renewal check failed", e);
}
}, 1, renewalCheckInterval, TimeUnit.MILLISECONDS);
log.info("Auto-renewal scheduler started with {} hour interval", renewalCheckInterval / 3600000);
}
@Override
public void disableAutoRenewal() throws CertbotException {
log.info("Disabling auto-renewal");
renewalExecutor.shutdown();
try {
if (!renewalExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
renewalExecutor.shutdownNow();
}
} catch (InterruptedException e) {
renewalExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Private helper methods
private List<String> buildCertbotCommand(CertificateRequest request) {
List<String> command = new ArrayList<>();
command.add(certbotConfig.getExecutablePath());
command.add("certonly");
command.add("--non-interactive");
command.add("--agree-tos");
command.add("--email");
command.add(request.getEmail());
// Domain specification
if (request.getAdditionalDomains().isEmpty()) {
command.add("--domain");
command.add(request.getDomain());
} else {
command.add("--domain");
command.add(String.join(",", request.getAllDomains()));
}
// Authentication method
switch (request.getAuthentication()) {
case WEBROOT:
command.add("--webroot");
command.add("--webroot-path");
command.add(request.getWebrootPath());
break;
case STANDALONE:
command.add("--standalone");
break;
case DNS:
command.add("--dns");
// DNS plugins would require additional parameters
break;
default:
throw new IllegalArgumentException("Unsupported authentication method: " + request.getAuthentication());
}
// Server configuration
if (request.isTestMode()) {
command.add("--test-cert");
}
// Force renewal if requested
if (request.isForceRenewal()) {
command.add("--force-renewal");
}
// RSA key size
command.add("--rsa-key-size");
command.add(String.valueOf(certbotConfig.getRsaKeySize()));
return command;
}
private ExecutionResult executeCertbotCommand(List<String> command) throws IOException, InterruptedException {
log.debug("Executing certbot command: {}", String.join(" ", command));
CommandLine commandLine = new CommandLine(command.get(0));
for (int i = 1; i < command.size(); i++) {
commandLine.addArgument(command.get(i));
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
DefaultExecutor executor = new DefaultExecutor();
executor.setStreamHandler(streamHandler);
int exitCode;
try {
exitCode = executor.execute(commandLine);
} catch (ExecuteException e) {
exitCode = e.getExitValue();
}
String output = outputStream.toString();
String errorOutput = errorStream.toString();
if (exitCode != 0) {
log.warn("Certbot command failed with exit code: {}\nError: {}", exitCode, errorOutput);
}
return new ExecutionResult(exitCode, output, errorOutput);
}
private CertificateInfo loadCertificateInfo(String domain) throws Exception {
String certPath = certbotConfig.getConfigDir() + "/live/" + domain + "/cert.pem";
String keyPath = certbotConfig.getConfigDir() + "/live/" + domain + "/privkey.pem";
String chainPath = certbotConfig.getConfigDir() + "/live/" + domain + "/fullchain.pem";
File certFile = new File(certPath);
File keyFile = new File(keyPath);
File chainFile = new File(chainPath);
if (!certFile.exists() || !keyFile.exists()) {
throw new CertbotException("Certificate files not found for domain: " + domain);
}
X509Certificate certificate = loadCertificateFromFile(certFile);
PrivateKey privateKey = loadPrivateKeyFromFile(keyFile);
CertificateInfo info = new CertificateInfo();
info.setDomain(domain);
info.setCertificateFile(certFile);
info.setPrivateKeyFile(keyFile);
info.setFullChainFile(chainFile);
info.setCertificate(certificate);
info.setPrivateKey(privateKey);
info.setExpiryDate(certificate.getNotAfter().toInstant());
info.setIssueDate(certificate.getNotBefore().toInstant());
info.setSerialNumber(certificate.getSerialNumber().toString());
info.setIssuer(certificate.getIssuerX500Principal().getName());
info.setStatus(calculateCertificateStatus(info));
return info;
}
private X509Certificate loadCertificateFromFile(File file) throws Exception {
try (FileInputStream fis = new FileInputStream(file)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(fis);
}
}
private PrivateKey loadPrivateKeyFromFile(File file) throws Exception {
String keyContent = Files.readString(file.toPath());
if (keyContent.contains("BEGIN RSA PRIVATE KEY")) {
// PKCS#1 format
keyContent = keyContent
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
} else if (keyContent.contains("BEGIN PRIVATE KEY")) {
// PKCS#8 format
keyContent = keyContent
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
} else {
throw new CertbotException("Unsupported private key format");
}
}
private CertificateStatus calculateCertificateStatus(CertificateInfo cert) {
if (cert.isExpired()) {
return CertificateStatus.EXPIRED;
} else if (cert.expiresSoon()) {
return CertificateStatus.EXPIRING_SOON;
} else {
return CertificateStatus.VALID;
}
}
private DeploymentResult deployToJavaKeystore(CertificateInfo cert) throws Exception {
log.info("Deploying certificate to Java keystore for domain: {}", cert.getDomain());
String keystorePath = certbotConfig.getDeployment().getKeystorePath();
String keystorePassword = certbotConfig.getDeployment().getKeystorePassword();
String keyAlias = certbotConfig.getDeployment().getKeyAlias();
// Load the full chain certificate
List<X509Certificate> certificateChain = loadCertificateChain(cert.getFullChainFile());
// Create or load keystore
KeyStore keyStore = loadOrCreateKeystore(keystorePath, keystorePassword);
// Convert certificate chain to array
Certificate[] certChain = certificateChain.toArray(new Certificate[0]);
// Set the key entry
keyStore.setKeyEntry(keyAlias, cert.getPrivateKey(), 
keystorePassword.toCharArray(), certChain);
// Save keystore
try (FileOutputStream fos = new FileOutputStream(keystorePath)) {
keyStore.store(fos, keystorePassword.toCharArray());
}
log.info("Certificate successfully deployed to Java keystore: {}", keystorePath);
Map<String, Object> details = new HashMap<>();
details.put("keystore_path", keystorePath);
details.put("key_alias", keyAlias);
details.put("certificates_in_chain", certificateChain.size());
return new DeploymentResult(true, "Certificate deployed to Java keystore", 
cert.getDomain(), DeploymentType.JAVA, Instant.now(), details);
}
private List<X509Certificate> loadCertificateChain(File chainFile) throws Exception {
List<X509Certificate> chain = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(chainFile)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = cf.generateCertificates(fis);
for (Certificate certificate : certificates) {
if (certificate instanceof X509Certificate) {
chain.add((X509Certificate) certificate);
}
}
}
return chain;
}
private KeyStore loadOrCreateKeystore(String keystorePath, String password) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
File keystoreFile = new File(keystorePath);
if (keystoreFile.exists()) {
try (FileInputStream fis = new FileInputStream(keystoreFile)) {
keyStore.load(fis, password.toCharArray());
}
} else {
// Create new keystore
keyStore.load(null, password.toCharArray());
// Create parent directories if they don't exist
keystoreFile.getParentFile().mkdirs();
}
return keyStore;
}
private DeploymentResult deployToNginx(CertificateInfo cert) throws CertbotException {
// Implementation for Nginx deployment
// This would typically update nginx configuration files
log.info("Nginx deployment not yet implemented for domain: {}", cert.getDomain());
return new DeploymentResult(false, "Nginx deployment not implemented", 
cert.getDomain(), DeploymentType.NGINX, Instant.now(), Collections.emptyMap());
}
private DeploymentResult deployToApache(CertificateInfo cert) throws CertbotException {
// Implementation for Apache deployment
log.info("Apache deployment not yet implemented for domain: {}", cert.getDomain());
return new DeploymentResult(false, "Apache deployment not implemented", 
cert.getDomain(), DeploymentType.APACHE, Instant.now(), Collections.emptyMap());
}
private DeploymentResult deployToDocker(CertificateInfo cert) throws CertbotException {
// Implementation for Docker deployment
log.info("Docker deployment not yet implemented for domain: {}", cert.getDomain());
return new DeploymentResult(false, "Docker deployment not implemented", 
cert.getDomain(), DeploymentType.DOCKER, Instant.now(), Collections.emptyMap());
}
private String buildDeployHookCommand() {
// Build a command that would be executed after certificate renewal
// This could restart web servers or reload configurations
return "systemctl reload nginx || true";
}
private List<String> parseRenewedDomains(String output) {
// Parse certbot output to extract renewed domains
List<String> domains = new ArrayList<>();
Pattern pattern = Pattern.compile("Congratulations! Your certificate for (.*?) has been renewed");
Matcher matcher = pattern.matcher(output);
while (matcher.find()) {
domains.add(matcher.group(1));
}
return domains;
}
private List<String> parseFailedDomains(String errorOutput) {
// Parse certbot error output to extract failed domains
List<String> domains = new ArrayList<>();
// Implementation depends on certbot error format
return domains;
}
private List<CertificateInfo> parseCertificatesJson(String jsonOutput) throws Exception {
// Parse the JSON output from 'certbot certificates --format json'
// This is a simplified implementation
List<CertificateInfo> certificates = new ArrayList<>();
try {
JsonNode root = objectMapper.readTree(jsonOutput);
if (root.isArray()) {
for (JsonNode certNode : root) {
CertificateInfo cert = new CertificateInfo();
cert.setDomain(certNode.path("domains").get(0).asText());
// Parse other fields...
certificates.add(cert);
}
}
} catch (Exception e) {
log.warn("Failed to parse certificates JSON, falling back to file scanning", e);
// Fall back to scanning certificate files directly
certificates = scanCertificateFiles();
}
return certificates;
}
private List<CertificateInfo> scanCertificateFiles() throws CertbotException {
List<CertificateInfo> certificates = new ArrayList<>();
File liveDir = new File(certbotConfig.getConfigDir() + "/live");
if (!liveDir.exists() || !liveDir.isDirectory()) {
return certificates;
}
File[] domainDirs = liveDir.listFiles(File::isDirectory);
if (domainDirs != null) {
for (File domainDir : domainDirs) {
try {
CertificateInfo cert = loadCertificateInfo(domainDir.getName());
certificates.add(cert);
} catch (Exception e) {
log.warn("Failed to load certificate info for domain: {}", domainDir.getName(), e);
}
}
}
return certificates;
}
private void validateCertificateRequest(CertificateRequest request) throws CertbotException {
if (request.getDomain() == null || request.getDomain().trim().isEmpty()) {
throw new CertbotException("Domain is required");
}
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
throw new CertbotException("Email is required");
}
// Validate domain format (basic check)
if (!request.getDomain().matches("^[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) {
throw new CertbotException("Invalid domain format: " + request.getDomain());
}
// Validate webroot path if using webroot authentication
if (request.getAuthentication() == AuthenticationMethod.WEBROOT) {
File webrootDir = new File(request.getWebrootPath());
if (!webrootDir.exists() || !webrootDir.isDirectory()) {
throw new CertbotException("Webroot directory does not exist: " + request.getWebrootPath());
}
if (!webrootDir.canWrite()) {
throw new CertbotException("Webroot directory is not writable: " + request.getWebrootPath());
}
}
}
private void validateDirectory(String path, String description) throws CertbotException {
File dir = new File(path);
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new CertbotException(description + " does not exist and cannot be created: " + path);
}
}
if (!dir.isDirectory()) {
throw new CertbotException(description + " is not a directory: " + path);
}
if (!dir.canRead() || !dir.canWrite()) {
throw new CertbotException(description + " is not accessible: " + path);
}
}
@PreDestroy
public void shutdown() {
log.info("Shutting down Certbot service");
disableAutoRenewal();
}
}
@Data
class ExecutionResult {
private final int exitCode;
private final String output;
private final String errorOutput;
public boolean isSuccess() {
return exitCode == 0;
}
}
5. Custom Exceptions
public class CertbotException extends Exception {
public CertbotException(String message) {
super(message);
}
public CertbotException(String message, Throwable cause) {
super(message, cause);
}
}
public class CertificateDeploymentException extends CertbotException {
public CertificateDeploymentException(String message) {
super(message);
}
public CertificateDeploymentException(String message, Throwable cause) {
super(message, cause);
}
}

REST API Controllers

1. Certificate Management API
@RestController
@RequestMapping("/api/certificates")
@Slf4j
@Validated
public class CertificateController {
private final CertbotService certbotService;
public CertificateController(CertbotService certbotService) {
this.certbotService = certbotService;
}
@PostMapping("/request")
public ResponseEntity<CertificateResult> requestCertificate(@RequestBody @Valid CertificateRequest request) {
try {
CertificateResult result = certbotService.requestCertificate(request);
return ResponseEntity.ok(result);
} catch (CertbotException e) {
log.error("Certificate request failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new CertificateResult(false, "Certificate request failed: " + e.getMessage(), 
null, Collections.emptyList(), Instant.now()));
}
}
@PostMapping("/renew")
public ResponseEntity<CertificateRenewalResult> renewCertificates(
@RequestParam(defaultValue = "false") boolean force) {
try {
CertificateRenewalResult result = certbotService.renewCertificates(force);
return ResponseEntity.ok(result);
} catch (CertbotException e) {
log.error("Certificate renewal failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new CertificateRenewalResult(false, "Certificate renewal failed: " + e.getMessage()));
}
}
@PostMapping("/{domain}/revoke")
public ResponseEntity<CertificateRevocationResult> revokeCertificate(
@PathVariable String domain,
@RequestParam(defaultValue = "UNSPECIFIED") RevocationReason reason) {
try {
CertificateRevocationResult result = certbotService.revokeCertificate(domain, reason);
return ResponseEntity.ok(result);
} catch (CertbotException e) {
log.error("Certificate revocation failed for domain: {}", domain, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new CertificateRevocationResult(false, "Certificate revocation failed: " + e.getMessage(), 
domain, reason, Instant.now()));
}
}
@GetMapping
public ResponseEntity<List<CertificateInfo>> listCertificates() {
try {
List<CertificateInfo> certificates = certbotService.listCertificates();
return ResponseEntity.ok(certificates);
} catch (CertbotException e) {
log.error("Failed to list certificates", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/{domain}")
public ResponseEntity<CertificateInfo> getCertificate(@PathVariable String domain) {
try {
Optional<CertificateInfo> certificate = certbotService.getCertificateInfo(domain);
return certificate.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
} catch (CertbotException e) {
log.error("Failed to get certificate for domain: {}", domain, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/expiring")
public ResponseEntity<List<CertificateInfo>> getExpiringCertificates(
@RequestParam(defaultValue = "30") int days) {
try {
List<CertificateInfo> certificates = certbotService.getExpiringCertificates(days);
return ResponseEntity.ok(certificates);
} catch (CertbotException e) {
log.error("Failed to get expiring certificates", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/{domain}/deploy")
public ResponseEntity<DeploymentResult> deployCertificate(@PathVariable String domain) {
try {
DeploymentResult result = certbotService.deployCertificate(domain);
return ResponseEntity.ok(result);
} catch (CertbotException e) {
log.error("Certificate deployment failed for domain: {}", domain, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new DeploymentResult(false, "Deployment failed: " + e.getMessage(), 
domain, DeploymentType.JAVA, Instant.now(), Collections.emptyMap()));
}
}
@PostMapping("/deploy-all")
public ResponseEntity<DeploymentResult> deployAllCertificates() {
try {
DeploymentResult result = certbotService.deployAllCertificates();
return ResponseEntity.ok(result);
} catch (CertbotException e) {
log.error("Bulk certificate deployment failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new DeploymentResult(false, "Bulk deployment failed: " + e.getMessage(), 
"all", DeploymentType.JAVA, Instant.now(), Collections.emptyMap()));
}
}
}
2. Certificate Health Check
@Component
public class CertificateHealthIndicator implements HealthIndicator {
private final CertbotService certbotService;
public CertificateHealthIndicator(CertbotService certbotService) {
this.certbotService = certbotService;
}
@Override
public Health health() {
try {
List<CertificateInfo> certificates = certbotService.listCertificates();
List<CertificateInfo> expiring = certbotService.getExpiringCertificates(7); // 7 days
Health.Builder builder = expiring.isEmpty() ? Health.up() : Health.down();
Map<String, Object> details = new HashMap<>();
details.put("totalCertificates", certificates.size());
details.put("expiringSoon", expiring.size());
details.put("expiringDomains", expiring.stream()
.map(CertificateInfo::getDomain)
.collect(Collectors.toList()));
details.put("checkTime", Instant.now());
return builder.withDetails(details).build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
3. Actuator Endpoint
@Component
@Endpoint(id = "certificates")
@Slf4j
public class CertificatesActuatorEndpoint {
private final CertbotService certbotService;
public CertificatesActuatorEndpoint(CertbotService certbotService) {
this.certbotService = certbotService;
}
@ReadOperation
public CertificateSummary getCertificateSummary() {
try {
List<CertificateInfo> certificates = certbotService.listCertificates();
List<CertificateInfo> expiring = certbotService.getExpiringCertificates(30);
Map<String, Object> details = new HashMap<>();
details.put("totalCertificates", certificates.size());
details.put("expiringWithin30Days", expiring.size());
details.put("domains", certificates.stream()
.map(CertificateInfo::getDomain)
.collect(Collectors.toList()));
return new CertificateSummary(
certificates.size(),
expiring.size(),
certificates.stream().filter(CertificateInfo::isExpired).count(),
Instant.now(),
details
);
} catch (Exception e) {
log.error("Failed to get certificate summary", e);
return new CertificateSummary(0, 0, 0, Instant.now(), 
Map.of("error", e.getMessage()));
}
}
@WriteOperation
public OperationResult renewCertificates(@Selector String operation) {
if ("renew".equals(operation)) {
try {
CertificateRenewalResult result = certbotService.renewCertificates(false);
return new OperationResult(result.isSuccess(), result.getMessage());
} catch (CertbotException e) {
return new OperationResult(false, "Renewal failed: " + e.getMessage());
}
}
return new OperationResult(false, "Unknown operation: " + operation);
}
@Data
public static class CertificateSummary {
private final long totalCertificates;
private final long expiringSoon;
private final long expired;
private final Instant checkTime;
private final Map<String, Object> details;
}
@Data
public static class OperationResult {
private final boolean success;
private final String message;
private final Instant timestamp = Instant.now();
}
}

Spring Boot Configuration

@SpringBootApplication
@EnableConfigurationProperties(CertbotConfig.class)
public class CertbotApplication {
public static void main(String[] args) {
SpringApplication.run(CertbotApplication.class, args);
}
@Bean
public CertbotService certbotService(CertbotConfig certbotConfig,
ObjectMapper objectMapper,
MeterRegistry meterRegistry) {
return new CertbotServiceImpl(certbotConfig, objectMapper, meterRegistry);
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class CertbotServiceTest {
@Mock
private CertbotConfig certbotConfig;
@Mock
private MeterRegistry meterRegistry;
@InjectMocks
private CertbotServiceImpl certbotService;
@Test
void testCertificateRequest() throws Exception {
// Given
CertificateRequest request = new CertificateRequest();
request.setDomain("example.com");
request.setEmail("[email protected]");
request.setAuthentication(AuthenticationMethod.WEBROOT);
// When & Then
// Test implementation would mock command execution
}
@Test
void testCertificateRenewal() throws Exception {
// Given
when(certbotConfig.getExecutablePath()).thenReturn("/usr/bin/certbot");
// When & Then
// Test renewal logic
}
}
@SpringBootTest
@TestPropertySource(properties = {
"app.certbot.enabled=false", // Disable for testing
"app.certbot.executable-path=/usr/bin/certbot"
})
class CertificateIntegrationTest {
@Autowired
private CertbotService certbotService;
@Test
void testServiceInitialization() {
assertNotNull(certbotService);
}
}

Best Practices

  1. Staging Environment: Always test with Let's Encrypt staging server first
  2. Rate Limits: Be aware of Let's Encrypt rate limits (5 certificates per domain per week)
  3. Backup: Regularly backup certificate files and configuration
  4. Monitoring: Monitor certificate expiration and renewal status
  5. Security: Secure private keys and use strong passphrases
// Example of rate limit handling
@Component
@Slf4j
public class RateLimitManager {
private final Map<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
private final Map<String, Instant> lastReset = new ConcurrentHashMap<>();
public boolean canRequestCertificate(String domain) {
String key = getCurrentPeriodKey();
AtomicInteger count = requestCounts.computeIfAbsent(key, k -> new AtomicInteger(0));
if (count.get() >= 5) { // Let's Encrypt limit
log.warn("Rate limit exceeded for domain: {} in period: {}", domain, key);
return false;
}
count.incrementAndGet();
return true;
}
private String getCurrentPeriodKey() {
return Instant.now().truncatedTo(ChronoUnit.DAYS).toString();
}
}

Conclusion

Certbot integration in Java provides:

  • Automated SSL certificate management with Let's Encrypt
  • Zero-downtime certificate renewal and deployment
  • Comprehensive certificate lifecycle management
  • Integration with various deployment targets (Java, Nginx, Apache)
  • Monitoring and health checks for certificate status

This implementation enables robust SSL/TLS certificate management for Java applications, supporting both standalone usage and integration with existing infrastructure. The solution handles the complete certificate lifecycle from issuance to renewal and deployment while providing comprehensive monitoring and management capabilities.

Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)

https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.

https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.

https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.

https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.

https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.

https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.

https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.

https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.

https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper