Migrating HTTP APIs to HTTPS in Spring Boot - Planning & Implementation
Overview
Converting HTTP APIs to HTTPS is one of the most critical security decisions in API management. This guide focuses on the planning, communication, and practical implementation of HTTP-to-HTTPS migration for Spring Boot applications.
For detailed information on SSL/TLS certificates, keystores, and trust management fundamentals, see the SSL/TLS Certificates & Trust Management Guide.
For complete Spring Boot SSL/TLS configuration patterns, see the Spring Boot SSL/TLS Configuration Guide.
Phase 1: Planning & Assessment
1. Impact Analysis Checklist
Before starting migration, assess the following:
| Area | Considerations | Action |
|---|---|---|
| API Clients | How many applications consume this API? | Inventory all consumers |
| Environment | Do all environments need migration simultaneously? | Plan staged approach |
| Certificates | Do you have valid certificates for all domains? | Procure/generate certs |
| Backward Compatibility | Can you maintain HTTP during transition? | Plan dual-port support |
| Performance | Will TLS handshake impact latency? | Benchmark and measure |
| Downtime | Can the API have scheduled downtime? | Plan rolling deployment |
| Dependencies | What downstream services depend on this API? | Update their configurations |
| Certificates Authority | Self-signed, internal CA, or public CA? | Determine trust model |
3. Decision Points
Single Port vs. Dual Port Migration
Dual Port Approach (Recommended for Safety)
- API runs on both
:8080(HTTP) and:8443(HTTPS) - Allows gradual client migration
- Lower risk of breaking integrations
Single Port Approach (Hard Cutover)
- Only HTTPS on
:8443 - Requires all clients ready simultaneously
- Higher risk but simpler operations
Phase 3: Implementation
Basic Setup: Simple GET API with HTTP and HTTPS
Step 1: Create a Simple Controller
@RestController
@RequestMapping("/api/v1")
@Slf4j
public class DemoApiController {
@GetMapping("/health")
public ResponseEntity<ApiResponse> health() {
return ResponseEntity.ok(new ApiResponse(
"success",
"API is healthy",
System.currentTimeMillis()
));
}
@PostMapping("/echo")
public ResponseEntity<ApiResponse> echo(@RequestBody EchoRequest request) {
return ResponseEntity.ok(new ApiResponse(
"success",
"Echo: " + request.getMessage(),
System.currentTimeMillis()
));
}
}
@Data
@AllArgsConstructor
class ApiResponse {
private String status;
private String message;
private Long timestamp;
}
@Data
class EchoRequest {
private String message;
}
Step 2: Configure Dual HTTP/HTTPS Ports (application.yml)
# HTTPS Configuration (Primary)
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: changeit
key-store-type: PKCS12
key-alias: myserver
protocol: TLS
enabled-protocols:
- TLSv1.2
- TLSv1.3
# HTTP Configuration (Secondary - for migration period)
server:
tomcat:
redirect-context-root: false
# Optional: HTTP to HTTPS redirect configuration
spring:
profiles:
active: prod
# Logging
logging:
level:
org.springframework.security: DEBUG
javax.net.ssl: DEBUG
Step 3: Enable HTTP-to-HTTPS Redirect with Dual Connectors
@Configuration
public class HttpsRedirectConfig {
/**
* For migration period: Allow both HTTP and HTTPS
* HTTP requests to specific paths will redirect to HTTPS
* This is the recommended approach for safe migration
*/
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
// Add HTTP connector that redirects to HTTPS
tomcat.addAdditionalTomcatConnectors(createHttpConnector());
return tomcat;
}
/**
* HTTP Connector - will redirect to HTTPS
* Port 8080: Available during migration period
* Will be removed after all clients migrate
*/
private Connector createHttpConnector() {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443); // Redirect to HTTPS port
// Optional: Only redirect specific paths
// Other paths could still be served over HTTP if needed
return connector;
}
/**
* Optional: Configure redirect rules if you want selective enforcement
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// For production, enforce HTTPS on all endpoints
http.requiresChannel()
.anyRequest()
.requiresSecure();
return http.build();
}
}
Step 4: Generate/Import Certificates
Option A: Self-Signed Certificate (Development)
# Generate PKCS12 keystore with self-signed certificate
keytool -genkeypair \
-alias myserver \
-keyalg RSA \
-keysize 2048 \
-keystore keystore.p12 \
-storetype PKCS12 \
-storepass changeit \
-dname "CN=localhost,OU=Development,O=MyOrg,L=City,ST=State,C=US" \
-validity 365
# Place in src/main/resources/
Option B: Use Existing Certificate from CA
# If you have certificate from CA:
# 1. You'll have: certificate.crt, private-key.key
# Convert to PKCS12 format
openssl pkcs12 -export \
-in certificate.crt \
-inkey private-key.key \
-out keystore.p12 \
-name myserver \
-passout pass:changeit
# Place in src/main/resources/
Phase 4: Testing HTTP and HTTPS Endpoints
Using cURL for Testing
Test HTTP Endpoint (During Migration)
# Should return 301 redirect or 200 OK (depending on config)
curl -v http://localhost:8080/api/v1/health
# Follow redirect automatically
curl -L http://localhost:8080/api/v1/health
Test HTTPS Endpoint (Production)
# Test with self-signed cert (ignore verification)
curl -k -v https://localhost:8443/api/v1/health
# Test with proper certificate verification
curl --cacert truststore.crt https://localhost:8443/api/v1/health
Test POST Request
# HTTP (during migration)
curl -X POST http://localhost:8080/api/v1/echo \
-H "Content-Type: application/json" \
-d '{"message":"Hello from HTTP"}' \
-L
# HTTPS (self-signed)
curl -k -X POST https://localhost:8443/api/v1/echo \
-H "Content-Type: application/json" \
-d '{"message":"Hello from HTTPS"}'
Using Java RestTemplate for Testing
@Service
public class ApiMigrationTest {
private final RestTemplate insecureRestTemplate;
@Autowired
public ApiMigrationTest(RestTemplate insecureRestTemplate) {
this.insecureRestTemplate = insecureRestTemplate;
}
/**
* Test HTTP endpoint during migration
*/
public void testHttpEndpoint() {
String httpUrl = "http://localhost:8080/api/v1/health";
try {
ResponseEntity<ApiResponse> response = insecureRestTemplate.getForEntity(
httpUrl,
ApiResponse.class
);
System.out.println("HTTP Status: " + response.getStatusCode());
System.out.println("Response: " + response.getBody());
} catch (Exception e) {
System.err.println("HTTP test failed: " + e.getMessage());
}
}
/**
* Test HTTPS endpoint
*/
public void testHttpsEndpoint() {
String httpsUrl = "https://localhost:8443/api/v1/health";
try {
ResponseEntity<ApiResponse> response = insecureRestTemplate.getForEntity(
httpsUrl,
ApiResponse.class
);
System.out.println("HTTPS Status: " + response.getStatusCode());
System.out.println("Response: " + response.getBody());
} catch (Exception e) {
System.err.println("HTTPS test failed: " + e.getMessage());
}
}
/**
* Test POST with payload
*/
public void testPostEndpoint() {
String url = "https://localhost:8443/api/v1/echo";
EchoRequest request = new EchoRequest();
request.setMessage("Test message");
ResponseEntity<ApiResponse> response = insecureRestTemplate.postForEntity(
url,
request,
ApiResponse.class
);
System.out.println("POST Response: " + response.getBody());
}
}
Phase 5: Client-Side Migration Guide
For Downstream Java/Spring Boot Clients
Before Migration: Verify Dependencies
<!-- Ensure HTTP client library is available -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- For advanced SSL handling -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
Update API Configuration
# Before (HTTP)
api:
base-url: http://api-server:8080
# After (HTTPS)
api:
base-url: https://api-server:8443
Update RestTemplate Configuration
Old Code (HTTP)
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
New Code (HTTPS with Certificate)
@Configuration
public class ApiClientConfig {
@Bean
public RestTemplate restTemplate() throws Exception {
// For self-signed certificates (development only)
return createInsecureRestTemplate();
// For production with proper certificates
// return createSecureRestTemplate();
}
/**
* For development/testing with self-signed certificates
* WARNING: Never use in production!
*/
private RestTemplate createInsecureRestTemplate() throws Exception {
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE
);
HttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(csf)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(requestFactory);
}
/**
* For production with proper certificate validation
*/
private RestTemplate createSecureRestTemplate() throws Exception {
// Load server's CA certificate
KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(
new FileInputStream("server-truststore.p12"),
"changeit".toCharArray()
);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
);
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, tmf.getTrustManagers(), null);
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
sslContext,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()
);
HttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(csf)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(requestFactory);
}
}
Add API Client Service
@Service
public class RemoteApiService {
private final RestTemplate restTemplate;
@Value("${api.base-url}")
private String apiBaseUrl;
public RemoteApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public ApiResponse callHealth() {
String url = apiBaseUrl + "/api/v1/health";
try {
ResponseEntity<ApiResponse> response = restTemplate.getForEntity(
url,
ApiResponse.class
);
return response.getBody();
} catch (ResourceAccessException e) {
System.err.println("Connection failed: " + e.getMessage());
throw new RuntimeException("Failed to connect to remote API", e);
}
}
public ApiResponse sendMessage(String message) {
String url = apiBaseUrl + "/api/v1/echo";
EchoRequest request = new EchoRequest();
request.setMessage(message);
ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
url,
request,
ApiResponse.class
);
return response.getBody();
}
}
Phase 6: Certificate Management & Expiry Planning
Certificate Monitoring
@Component
@Slf4j
public class CertificateMonitoring {
@Scheduled(fixedRate = 86400000) // Daily check
public void checkCertificateExpiry() {
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(
new FileInputStream("keystore.p12"),
"changeit".toCharArray()
);
X509Certificate cert = (X509Certificate)
keyStore.getCertificate("myserver");
Date expiryDate = cert.getNotAfter();
long daysUntilExpiry =
(expiryDate.getTime() - System.currentTimeMillis())
/ (1000 * 86400);
if (daysUntilExpiry < 30) {
log.error("Certificate expires in {} days - CRITICAL", daysUntilExpiry);
// Alert administrators
} else if (daysUntilExpiry < 90) {
log.warn("Certificate expires in {} days", daysUntilExpiry);
} else {
log.info("Certificate valid for {} days", daysUntilExpiry);
}
} catch (Exception e) {
log.error("Failed to check certificate expiry", e);
}
}
}
Renewal Planning
| Timeline | Action |
|---|---|
| 90 days before | Request new certificate from CA |
| 30 days before | Notify all consumers of upcoming renewal |
| 7 days before | Generate new keystore and coordinate deployment |
| 1 day before | Final testing with new certificate |
| On renewal day | Deploy new keystore, monitor for errors |
| After renewal | Verify all clients are still connected |
Common Errors & Troubleshooting
Error 1: CERTIFICATE_VERIFY_FAILED
Cause: Client cannot verify server certificate
Solution:
// Development only: Ignore verification
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
// Production: Add server certificate to truststore
KeyStore trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(new FileInputStream("truststore.p12"),
"password".toCharArray());
Error 2: HOSTNAME_MISMATCH
Cause: Certificate hostname doesn’t match request hostname
Solution:
// Ensure certificate CN matches hostname
// Certificate: CN=api.example.com
// Request: https://api.example.com:8443/api/v1/health ✓
// Wrong: https://api-internal:8443/api/v1/health ✗
// (hostname doesn't match certificate)
// Use hostname verifier
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
sslContext,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()
);
Error 3: UNABLE_TO_FIND_VALID_CERTIFICATION_PATH
Cause: Certificate chain not trusted
Solution:
# Export server certificate
openssl s_client -connect api-server:8443 -showcerts > cert.pem
# Import to client truststore
keytool -import -alias server-cert -file cert.pem \
-keystore truststore.p12 -storepass changeit
Error 4: 307 Redirect Loop
Cause: HTTP to HTTPS redirect misconfigured
Solution:
# Ensure ports are different
server:
port: 8443 # HTTPS
tomcat:
redirect-context-root: false
# Create explicit redirect rule
http:
port: 8080
redirect-to-https: true
For detailed SSL/TLS configuration and advanced topics, see: