Context
This project is the Java 21 + Spring Boot control-plane side of the broader IoT audit platform migration described in Migrating an IoT Audit Engine into a Java 21 + Spring Boot Platform.
The public repository is available on GitHub:
ThomasBonderup/certificate-control-plane-api
The problem it solves is operational rather than cryptographic: once an embedded or IoT fleet grows, certificate lifecycle work quickly becomes more than “store a certificate somewhere.” Teams need to know which certificates exist, which tenant owns them, which assets depend on them, which ones are close to expiry, who owns the renewal, what state the renewal is in, and which workflow changes should become durable audit evidence.
The service is intentionally scoped as a control plane, not a certificate parser or audit execution engine. It owns platform state around certificates, assets, bindings, workflow status, tenant boundaries, API documentation, and operational integration. Technical probe execution, TLS validation, finding generation, and report logic remain outside this service.
That separation keeps audit execution and platform workflow concerns in different planes, then makes the boundary clear through APIs, schema migrations, tests, and documentation.
What The API Does
The repository implements a Spring Boot API for certificate lifecycle management in embedded and IoT environments:
- Certificate inventory with status, validity window, owner, notes, fingerprint, issuer, serial number, and renewal workflow fields.
- Asset inventory with tenant, type, environment, hostname, location, and audit fields.
- Certificate-to-asset bindings modeled as first-class records with binding type, endpoint, port, and uniqueness constraints.
- Reverse lookup APIs from asset to certificate bindings and bound certificates.
- Expiring-soon and attention-needed views for certificates that are expired, blocked, approaching expiry, unowned, or not progressing through renewal.
- Summary endpoints for tenant-scoped certificate posture.
- Renewal history APIs backed by persisted status-change rows.
- Evidence ingestion foundations that accept raw probe evidence and publish envelopes to Kafka.
The controllers are documented with OpenAPI annotations and exposed through local Swagger UI. The committed Postman collection covers token acquisition, authorization failures, certificate/asset/binding flows, and protected actuator endpoints.
Spring Boot Architecture
The implementation uses a conventional but production-minded Spring Boot structure:
controllerclasses own HTTP routes, validation entry points, response codes,Locationheaders, pagination defaults, and OpenAPI metadata.apirecords define request and response DTOs with validation annotations and Swagger schema examples.serviceshold application logic, transaction boundaries, tenant enforcement, renewal workflow rules, outbox writing, and event publishing.repositoriesuse Spring Data JPA derived queries and JPQL for tenant-scoped filters, expiring-soon queries, attention-needed queries, and summary counts.modelclasses define JPA entities and enums for certificates, assets, bindings, renewal history, evidence envelopes, and outbox events.commonandconfigprovide shared API errors, JSON security errors, tenant/user providers, pageable sanitization, OpenAPI setup, scheduling, and Spring Security.
This makes the project easy to review because the core responsibilities are explicit: request shape, security, business rule, persistence model, integration point, and test coverage all have visible homes.
Security, Tenancy, And Authorization
Security is not only described in the README; it is implemented and tested:
- The service runs as a stateless OAuth2 resource server.
- Local development uses Keycloak with a rendered realm template and JWTs that include a
tenantIdclaim. - Spring Security maps scopes into authorities and extracts Keycloak realm/client roles into
ROLE_*authorities. GET /api/**requirescontrolplane.read;POST,PATCH, andDELETE /api/**requirecontrolplane.write.- Actuator health and info are public; metrics and Prometheus require
controlplane.read. @EnableMethodSecurityenables method-level authorization.- Certificate and asset delete operations are protected with
@PreAuthorize("hasRole('ADMIN')"). - Tenant-aware services use the authenticated
tenantIdclaim as the real boundary. - Create requests that carry a
tenantIdmust match the JWT claim. - Cross-tenant resource access is intentionally hidden as not found.
The test suite covers the important failure modes: missing token, wrong scope, write token trying to read, read token trying to write, non-admin delete attempts, cross-tenant lookup behavior, and mismatched tenant input.
Persistence And Schema Evolution
The persistence layer is built around PostgreSQL and Flyway rather than ad hoc schema creation:
certificatesstores certificate metadata, tenant ownership, status, validity timestamps, owner, notes, audit fields, blocked reason, and renewal update timestamp.assetsstores tenant-scoped operational assets with type, environment, hostname, location, and audit fields.certificate_bindingsmodels certificate-to-asset relationships with a uniqueness constraint across certificate, asset, binding type, endpoint, and port.certificate_renewal_status_historystores workflow transitions for later inspection.outbox_eventsstores pending event payloads as PostgreSQLjsonbwith aggregate metadata, topic, key, status, and publication timestamps.- Hibernate validates the schema at startup with
ddl-auto: validate, while Flyway owns schema changes. - Indexes support tenant, status, expiry, asset, binding, history, and outbox lookup paths.
That matters because certificate lifecycle platforms live or die on boring correctness: schema ownership, tenant filters, predictable queries, and auditable state transitions.
Kafka, Evidence, And Outbox
The eventing implementation has three useful parts:
- Raw evidence ingestion accepts probe evidence, maps it into an
EvidenceEnvelope, and publishes it to Kafka. - Renewal status changes are represented as
CertificateRenewalStatusChangedEventrecords. - Renewal status updates append an outbox event inside the certificate update flow, and an
OutboxPublisherpublishes pending rows to Kafka and marks them as published.
There is also a Kafka consumer that persists renewal status change events into certificate_renewal_status_history.
The outbox pattern is used here for reliability around renewal workflow events.1 Instead of only updating the certificate row and trying to publish directly to Kafka as a separate best-effort side effect, the service records a pending event in the database as part of the update flow. A publisher can then read pending rows, publish them, and mark them as published. That gives the system a durable handoff point between database state and asynchronous event delivery.
The important nuance: this is an outbox-backed flow for renewal status publication, not a claim that every asynchronous path in the service is outbox-backed. Raw evidence ingestion still publishes directly to Kafka. That distinction keeps the implementation boundary honest while showing where reliability mattered most for lifecycle audit events.
Documentation And Local Developer Experience
The repository is set up to be reviewed, run, and manually exercised:
README.mdexplains the service scope, local setup, Keycloak token flows, tenant enforcement rules, OpenAPI, Swagger UI, Postman usage, actuator endpoints, and security expectations.specs/architecture.mddocuments the control-plane/execution-plane split, domain model, layered structure, persistence model, and current API surface.docker-compose.ymlstarts PostgreSQL, Kafka, Keycloak, the API container, and optional Kafka UI with local-only port bindings..env.exampleand Postman environment templates are sanitized.- Swagger UI is available locally at
/swagger-ui.html, backed by/v3/api-docs. - The Postman collection includes read/write/admin token flows, no-token demos, read/write failure demos, certificate/asset/binding requests, and protected actuator checks.
That kind of documentation is a delivery signal: the backend is not only implemented; it is packaged so another engineer can evaluate the behavior quickly.
Test Coverage
The automated test suite is one of the strongest parts of the project.
./gradlew test passes, and the generated JUnit XML reports 88 tests across 13 test classes. The coverage is not just happy-path CRUD:
- MockMvc integration tests for certificates, assets, bindings, audit runs, and authorization behavior.
- Testcontainers PostgreSQL integration coverage for persistence-backed API behavior.
- Spring Security test support for JWT scopes, tenant claims, roles, and protected actuator endpoints.
- Tests for pagination, sorting, malformed Swagger sort fallback, filters, summary counts, expiry windows, attention-needed rules, renewal workflow transitions, and tenant isolation.
- Service tests for renewal-status outbox writing.
- Tests for Kafka producer configuration, renewal-status consumer persistence, outbox event writing, and outbox publication ordering.
I describe the test suite by the behavior it proves, not by a headline percentage, because the repository does not currently publish a separate code coverage report.
Outcome
The result is a public backend project that demonstrates:
- Java 21 and Spring Boot platform delivery.
- REST API design with validation, response DTOs, pageable/sortable endpoints, and clean HTTP semantics.
- Spring Security fundamentals with JWT resource server configuration, Keycloak role extraction, scope checks, method-level authorization, and JSON security errors.
- Tenant-aware service design with explicit claim enforcement and cross-tenant protections.
- PostgreSQL/Flyway schema ownership with JPA/Hibernate validation.
- Kafka integration, evidence ingestion foundations, renewal history, and an outbox-backed event flow.
- OpenAPI, Swagger UI, Postman, Docker Compose, Keycloak local setup, Actuator, Prometheus metrics, and practical developer documentation.
- A meaningful automated test suite that proves behavior across web, service, persistence, security, and integration boundaries.
For technical context, read the related migration article or inspect the repository directly:
Read the Java migration article or view the public GitHub repository.
References
Footnotes
-
Chris Richardson, Transactional outbox pattern, Microservices.io. ↩