Secure Container Management: Integrating Java Applications with Harbor Registry Scanning

Harbor is an open-source cloud native registry that stores, signs, and scans container images for vulnerabilities. When combined with Java applications in containerized environments, Harbor provides enterprise-grade security features that are essential for modern DevOps pipelines. This article explores how to integrate Java applications with Harbor's vulnerability scanning capabilities programmatically.


Understanding Harbor's Security Ecosystem

Key Harbor Components:

  • Registry: Docker-compliant container image storage
  • Vulnerability Scanning: Integrated security scanning using Trivy, Clair, or Anchore
  • Replication: Multi-registry synchronization
  • RBAC: Role-based access control
  • Signing & Verification: Content trust with Notary
  • Webhook Support: Event notifications for automated workflows

Harbor Scanning Workflow:

Java App → Docker Image → Push to Harbor → Automatic Scan → Vulnerability Report → Decision Gate

Prerequisites

Before integrating with Harbor, ensure you have:

  1. Harbor Instance with scanning enabled
  2. Administrator Access for API operations
  3. Java 11+ with HTTP client capabilities
  4. Docker for building Java application images
  5. Project Creation in Harbor for your images

Harbor API Overview

Harbor provides a comprehensive REST API for all operations:

// Base API endpoints
String HARBOR_BASE = "https://harbor.example.com/api/v2.0";
String SCAN_BASE = HARBOR_BASE + "/scans";
String PROJECT_BASE = HARBOR_BASE + "/projects";
String REPOSITORY_BASE = HARBOR_BASE + "/repositories";

Java Client for Harbor Integration

Maven Dependencies:

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>

Core Harbor Client Implementation

package com.example.harbor.client;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import java.io.IOException;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class HarborClient {
private final String harborBaseUrl;
private final String username;
private final String password;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
public HarborClient(String harborBaseUrl, String username, String password) {
this.harborBaseUrl = harborBaseUrl.endsWith("/") ? 
harborBaseUrl.substring(0, harborBaseUrl.length() - 1) : harborBaseUrl;
this.username = username;
this.password = password;
this.objectMapper = new ObjectMapper();
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build();
}
private String getAuthHeader() {
String credentials = username + ":" + password;
return "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
}
private Request.Builder authenticatedRequest(String url) {
return new Request.Builder()
.url(url)
.header("Authorization", getAuthHeader())
.header("Content-Type", "application/json");
}
/**
* Check if a project exists, create if it doesn't
*/
public void ensureProjectExists(String projectName) throws IOException {
String url = harborBaseUrl + "/api/v2.0/projects?name=" + projectName;
Request request = authenticatedRequest(url).get().build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.code() == 200) {
String responseBody = response.body().string();
List<Project> projects = objectMapper.readValue(responseBody, 
new TypeReference<List<Project>>() {});
if (projects.isEmpty()) {
createProject(projectName);
}
}
}
}
/**
* Create a new project in Harbor
*/
public void createProject(String projectName) throws IOException {
String url = harborBaseUrl + "/api/v2.0/projects";
ProjectRequest projectRequest = new ProjectRequest(projectName);
String jsonBody = objectMapper.writeValueAsString(projectRequest);
Request request = authenticatedRequest(url)
.post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.code() != 201) {
throw new IOException("Failed to create project: " + response.body().string());
}
}
}
/**
* Get vulnerability scan report for an image
*/
public ScanReport getVulnerabilityReport(String projectName, 
String repositoryName, 
String reference) throws IOException {
String url = harborBaseUrl + "/api/v2.0/projects/" + projectName + 
"/repositories/" + repositoryName + "/artifacts/" + reference + 
"/additions/vulnerabilities";
Request request = authenticatedRequest(url).get().build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.code() == 200) {
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, ScanReport.class);
} else {
throw new IOException("Failed to get vulnerability report: " + 
response.code() + " - " + response.body().string());
}
}
}
/**
* Start a manual scan of an artifact
*/
public void startArtifactScan(String projectName, String repositoryName, 
String reference) throws IOException {
String url = harborBaseUrl + "/api/v2.0/projects/" + projectName + 
"/repositories/" + repositoryName + "/artifacts/" + reference + 
"/scan";
Request request = authenticatedRequest(url)
.post(RequestBody.create("", MediaType.parse("application/json")))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.code() != 202) {
throw new IOException("Failed to start scan: " + response.body().string());
}
}
}
/**
* Check scan summary for an artifact
*/
public ScanSummary getScanSummary(String projectName, String repositoryName, 
String reference) throws IOException {
String url = harborBaseUrl + "/api/v2.0/projects/" + projectName + 
"/repositories/" + repositoryName + "/artifacts/" + reference;
Request request = authenticatedRequest(url).get().build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.code() == 200) {
String responseBody = response.body().string();
Artifact artifact = objectMapper.readValue(responseBody, Artifact.class);
return artifact.scanOverview;
} else {
throw new IOException("Failed to get scan summary: " + response.body().string());
}
}
}
// Data classes for Harbor API responses
public static class ProjectRequest {
@JsonProperty("project_name")
public String projectName;
@JsonProperty("public")
public boolean isPublic = true;
public ProjectRequest(String projectName) {
this.projectName = projectName;
}
}
public static class Project {
@JsonProperty("project_id")
public int projectId;
@JsonProperty("name")
public String name;
}
public static class ScanReport {
@JsonProperty("vulnerabilities")
public List<Vulnerability> vulnerabilities;
@JsonProperty("summary")
public ReportSummary summary;
}
public static class Vulnerability {
@JsonProperty("id")
public String id;
@JsonProperty("package")
public String packageName;
@JsonProperty("version")
public String version;
@JsonProperty("fix_version")
public String fixVersion;
@JsonProperty("severity")
public String severity;
@JsonProperty("description")
public String description;
}
public static class ReportSummary {
@JsonProperty("total")
public int total;
@JsonProperty("fixable")
public int fixable;
@JsonProperty("summary")
public SeveritySummary severitySummary;
}
public static class SeveritySummary {
@JsonProperty("Critical")
public int critical;
@JsonProperty("High")
public int high;
@JsonProperty("Medium")
public int medium;
@JsonProperty("Low")
public int low;
@JsonProperty("Unknown")
public int unknown;
@JsonProperty("None")
public int none;
}
public static class Artifact {
@JsonProperty("scan_overview")
public ScanSummary scanOverview;
}
public static class ScanSummary {
@JsonProperty("scan_status")
public String scanStatus;
@JsonProperty("severity")
public String severity;
@JsonProperty("components")
public ScanComponents components;
}
public static class ScanComponents {
@JsonProperty("total")
public int total;
@JsonProperty("summary")
public List<SeverityCount> summary;
}
public static class SeverityCount {
@JsonProperty("severity")
public String severity;
@JsonProperty("count")
public int count;
}
}

Java Application Image Builder

package com.example.harbor.builder;
import java.io.*;
import java.nio.file.*;
import java.util.concurrent.TimeUnit;
public class DockerImageBuilder {
/**
* Build Docker image for a Java application
*/
public String buildJavaAppImage(String projectDir, String imageName, String imageTag) 
throws IOException, InterruptedException {
// Create Dockerfile for Java application
createDockerfile(projectDir);
// Build image
ProcessBuilder builder = new ProcessBuilder(
"docker", "build", 
"-t", imageName + ":" + imageTag,
"."
);
builder.directory(new File(projectDir));
Process process = builder.start();
logProcessOutput(process);
boolean completed = process.waitFor(5, TimeUnit.MINUTES);
if (!completed || process.exitValue() != 0) {
throw new IOException("Docker build failed");
}
return imageName + ":" + imageTag;
}
private void createDockerfile(String projectDir) throws IOException {
String dockerfileContent = """
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY target/*.jar app.jar
USER 1001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
""";
Files.writeString(Paths.get(projectDir, "Dockerfile"), dockerfileContent);
}
private void logProcessOutput(Process process) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Docker: " + line);
}
}
}
/**
* Push image to Harbor registry
*/
public void pushImageToHarbor(String harborUrl, String projectName, 
String imageName, String imageTag) 
throws IOException, InterruptedException {
String fullImageName = harborUrl + "/" + projectName + "/" + imageName + ":" + imageTag;
// Tag image for Harbor
ProcessBuilder tagBuilder = new ProcessBuilder(
"docker", "tag", 
imageName + ":" + imageTag,
fullImageName
);
Process tagProcess = tagBuilder.start();
if (!tagProcess.waitFor(30, TimeUnit.SECONDS) || tagProcess.exitValue() != 0) {
throw new IOException("Docker tag failed");
}
// Push to Harbor
ProcessBuilder pushBuilder = new ProcessBuilder("docker", "push", fullImageName);
Process pushProcess = pushBuilder.start();
logProcessOutput(pushProcess);
boolean completed = pushProcess.waitFor(5, TimeUnit.MINUTES);
if (!completed || pushProcess.exitValue() != 0) {
throw new IOException("Docker push failed");
}
System.out.println("Successfully pushed image: " + fullImageName);
}
}

Security Gate Implementation

package com.example.harbor.security;
import com.example.harbor.client.HarborClient;
import com.example.harbor.client.HarborClient.ScanReport;
import com.example.harbor.client.HarborClient.ScanSummary;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class SecurityGate {
private final HarborClient harborClient;
private final SecurityPolicy securityPolicy;
public SecurityGate(HarborClient harborClient, SecurityPolicy securityPolicy) {
this.harborClient = harborClient;
this.securityPolicy = securityPolicy;
}
/**
* Wait for scan completion with timeout
*/
public void waitForScanCompletion(String projectName, String repositoryName,
String reference, long timeoutMinutes) 
throws IOException, InterruptedException {
long startTime = System.currentTimeMillis();
long timeoutMs = timeoutMinutes * 60 * 1000;
while (System.currentTimeMillis() - startTime < timeoutMs) {
ScanSummary summary = harborClient.getScanSummary(projectName, repositoryName, reference);
if ("Success".equals(summary.scanStatus) || "Error".equals(summary.scanStatus)) {
return; // Scan completed
}
System.out.println("Scan status: " + summary.scanStatus + ", waiting...");
TimeUnit.SECONDS.sleep(10);
}
throw new IOException("Scan timeout after " + timeoutMinutes + " minutes");
}
/**
* Evaluate image against security policy
*/
public SecurityEvaluation evaluateImage(String projectName, String repositoryName,
String reference) throws IOException {
// Wait for scan to complete
try {
waitForScanCompletion(projectName, repositoryName, reference, 10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new SecurityEvaluation(false, "Scan interrupted");
}
// Get vulnerability report
ScanReport report = harborClient.getVulnerabilityReport(projectName, repositoryName, reference);
// Apply security policy
return securityPolicy.evaluate(report);
}
public static class SecurityEvaluation {
private final boolean passed;
private final String message;
private final int criticalCount;
private final int highCount;
public SecurityEvaluation(boolean passed, String message, int criticalCount, int highCount) {
this.passed = passed;
this.message = message;
this.criticalCount = criticalCount;
this.highCount = highCount;
}
public SecurityEvaluation(boolean passed, String message) {
this(passed, message, 0, 0);
}
// Getters
public boolean isPassed() { return passed; }
public String getMessage() { return message; }
public int getCriticalCount() { return criticalCount; }
public int getHighCount() { return highCount; }
}
}

Security Policy Configuration

package com.example.harbor.security;
import com.example.harbor.client.HarborClient.ScanReport;
import com.example.harbor.client.HarborClient.ReportSummary;
import com.example.harbor.client.HarborClient.SeveritySummary;
public class SecurityPolicy {
private final int maxCritical;
private final int maxHigh;
private final boolean failOnUnknown;
private final List<String> allowedPackages;
private final List<String> bannedPackages;
public SecurityPolicy(int maxCritical, int maxHigh, boolean failOnUnknown) {
this.maxCritical = maxCritical;
this.maxHigh = maxHigh;
this.failOnUnknown = failOnUnknown;
this.allowedPackages = new ArrayList<>();
this.bannedPackages = new ArrayList<>();
}
public SecurityPolicy addAllowedPackage(String packagePattern) {
allowedPackages.add(packagePattern);
return this;
}
public SecurityPolicy addBannedPackage(String packagePattern) {
bannedPackages.add(packagePattern);
return this;
}
public SecurityGate.SecurityEvaluation evaluate(ScanReport report) {
if (report == null || report.summary == null) {
return new SecurityGate.SecurityEvaluation(false, "No scan report available");
}
ReportSummary summary = report.summary;
SeveritySummary severitySummary = summary.severitySummary;
// Check severity thresholds
if (severitySummary.critical > maxCritical) {
return new SecurityGate.SecurityEvaluation(false, 
"Critical vulnerabilities exceed policy: " + severitySummary.critical + " > " + maxCritical,
severitySummary.critical, severitySummary.high);
}
if (severitySummary.high > maxHigh) {
return new SecurityGate.SecurityEvaluation(false,
"High vulnerabilities exceed policy: " + severitySummary.high + " > " + maxHigh,
severitySummary.critical, severitySummary.high);
}
if (failOnUnknown && severitySummary.unknown > 0) {
return new SecurityGate.SecurityEvaluation(false,
"Unknown severity vulnerabilities found: " + severitySummary.unknown,
severitySummary.critical, severitySummary.high);
}
// Check banned packages
if (report.vulnerabilities != null) {
for (var vuln : report.vulnerabilities) {
if (isBannedPackage(vuln.packageName)) {
return new SecurityGate.SecurityEvaluation(false,
"Banned package detected: " + vuln.packageName,
severitySummary.critical, severitySummary.high);
}
}
}
return new SecurityGate.SecurityEvaluation(true,
"Image passed security policy. Critical: " + severitySummary.critical + 
", High: " + severitySummary.high,
severitySummary.critical, severitySummary.high);
}
private boolean isBannedPackage(String packageName) {
return bannedPackages.stream()
.anyMatch(banned -> packageName.matches(banned));
}
}

Complete Pipeline Example

package com.example.harbor.pipeline;
import com.example.harbor.builder.DockerImageBuilder;
import com.example.harbor.client.HarborClient;
import com.example.harbor.security.SecurityGate;
import com.example.harbor.security.SecurityPolicy;
import java.io.File;
public class HarborSecurityPipeline {
public static void main(String[] args) {
try {
// Configuration
String harborUrl = "https://harbor.example.com";
String username = "admin";
String password = "password";
String projectName = "java-apps";
String appName = "my-spring-app";
String appVersion = "1.0.0";
String projectDir = "/path/to/your/java/project";
// Initialize clients
HarborClient harborClient = new HarborClient(harborUrl, username, password);
DockerImageBuilder imageBuilder = new DockerImageBuilder();
// Define security policy
SecurityPolicy policy = new SecurityPolicy(
0,  // No critical vulnerabilities allowed
5,  // Maximum 5 high vulnerabilities
true // Fail on unknown severity
).addBannedPackage(".*log4j.*") // Ban log4j vulnerabilities
.addBannedPackage(".*commons-collections.*"); // Ban specific packages
SecurityGate securityGate = new SecurityGate(harborClient, policy);
// Execute pipeline
System.out.println("Starting Harbor security pipeline...");
// Ensure project exists
harborClient.ensureProjectExists(projectName);
// Build Java application image
String imageTag = imageBuilder.buildJavaAppImage(projectDir, appName, appVersion);
// Push to Harbor
imageBuilder.pushImageToHarbor(harborUrl, projectName, appName, appVersion);
// Trigger scan
harborClient.startArtifactScan(projectName, appName, appVersion);
// Evaluate security
SecurityGate.SecurityEvaluation result = securityGate.evaluateImage(
projectName, appName, appVersion);
// Output results
if (result.isPassed()) {
System.out.println("✅ Security check PASSED: " + result.getMessage());
System.exit(0);
} else {
System.out.println("❌ Security check FAILED: " + result.getMessage());
System.out.println("Critical: " + result.getCriticalCount() + 
", High: " + result.getHighCount());
System.exit(1);
}
} catch (Exception e) {
System.err.println("Pipeline failed: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}

Webhook Integration for Automated Scanning

package com.example.harbor.webhook;
import com.fasterxml.jackson.databind.ObjectMapper;
import spark.Spark;
public class HarborWebhookServer {
private final HarborClient harborClient;
private final SecurityGate securityGate;
private final ObjectMapper objectMapper;
public HarborWebhookServer(HarborClient harborClient, SecurityGate securityGate) {
this.harborClient = harborClient;
this.securityGate = securityGate;
this.objectMapper = new ObjectMapper();
}
public void start(int port) {
Spark.port(port);
Spark.post("/webhook/harbor", (request, response) -> {
try {
HarborWebhookPayload payload = objectMapper.readValue(
request.body(), HarborWebhookPayload.class);
handleWebhookEvent(payload);
response.status(200);
return "Webhook processed";
} catch (Exception e) {
response.status(500);
return "Error processing webhook: " + e.getMessage();
}
});
}
private void handleWebhookEvent(HarborWebhookPayload payload) {
if ("PUSH_ARTIFACT".equals(payload.type)) {
String projectName = payload.eventData.repository.namespace;
String repositoryName = payload.eventData.repository.name;
String reference = payload.eventData.resources[0].tag;
// Auto-scan on push
new Thread(() -> {
try {
harborClient.startArtifactScan(projectName, repositoryName, reference);
System.out.println("Auto-triggered scan for: " + repositoryName + ":" + reference);
} catch (Exception e) {
System.err.println("Failed to trigger scan: " + e.getMessage());
}
}).start();
}
}
public static class HarborWebhookPayload {
public String type;
public EventData eventData;
}
public static class EventData {
public Repository repository;
public Resource[] resources;
}
public static class Repository {
public String name;
public String namespace;
}
public static class Resource {
public String tag;
public String digest;
}
}

Best Practices for Harbor Integration

  1. Authentication: Use robot accounts for API access
  2. Retry Logic: Implement retries for network operations
  3. Timeouts: Set appropriate timeouts for scan operations
  4. Caching: Cache scan results to avoid repeated API calls
  5. Logging: Comprehensive logging for audit trails
  6. Error Handling: Graceful handling of Harbor API errors

Benefits for Java Applications

  • Early Vulnerability Detection: Catch security issues in CI/CD
  • Policy Enforcement: Ensure compliance with organizational standards
  • Automated Security: Reduce manual security reviews
  • Audit Trail: Complete record of image security states
  • Quality Gates: Prevent vulnerable images from reaching production

Conclusion

Integrating Java applications with Harbor's vulnerability scanning provides a robust security foundation for containerized deployments. By leveraging the approaches outlined in this article, you can:

  • Automate Security Scanning: Integrate vulnerability checks into your build pipeline
  • Enforce Policies: Implement custom security rules for your organization
  • Programmatically Manage Images: Control Harbor operations through Java code
  • React to Events: Use webhooks for real-time security responses

This integration moves security left in the development process, ensuring that Java applications are scanned, evaluated, and approved before they ever reach production environments, significantly reducing the attack surface of your containerized applications.

Leave a Reply

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


Macro Nepal Helper