Production-grade headless commerce backend engineered for automated DevSecOps pipelines
- Overview
- DevSecOps Pipeline Architecture
- Two-Phase Ephemeral Infrastructure
- Infrastructure Pattern Library
- Security Hardening
- Domain Data Model
- API Reference
- Project Structure
- Documentation
- Local Development
This project is a REST API backend for a headless e-commerce platform, designed as a full DevSecOps reference implementation. Security is treated as a first-class citizen β not a post-deployment afterthought.
Every pull request into main triggers a fully automated 6-job DAG validation pipeline that runs secret scanning, SAST analysis, infrastructure compliance checks, SCA/container hardening, ephemeral AWS deployment, OWASP ZAP DAST validation, guaranteed teardown, and a final branch-protection gate β all before a single line of code can be merged.
| Layer | Technology |
|---|---|
| Runtime | Java 17 (Eclipse Temurin), Spring Boot 3.5.x |
| Database | AWS RDS PostgreSQL 16, HikariCP connection pool |
| Infrastructure | AWS VPC Β· ALB Β· EC2 Auto Scaling Group Β· RDS |
| IaC | Terraform (modular, remote S3 backend state) |
| CI/CD | GitHub Actions (DAG multi-job pipeline) |
| Security Tools | Trufflehog Β· Semgrep Β· Checkov Β· Trivy Β· OWASP ZAP |
| Container | Docker (eclipse-temurin:17-jre-alpine base) |
| Auth | AWS OIDC Keyless Federation (no static credentials) |
Pull requests trigger a fully parallelized Directed Acyclic Graph (DAG) workflow in GitHub Actions. Uncoupled static checks run in parallel; rigid compliance gates block cloud provisioning until all upstream jobs pass.
[ git push β pull_request on main ]
β
βΌ
βββββββββββββββββββββββββββββββββββ
β JOB 1 β Static Security Scans β β Runs parallel to nothing; blocks all downstream
β β
β β Trufflehog (Secret Scan) β
β β Semgrep (SAST / OWASP) β
β β Checkov (IaC Compliance) β
βββββββββββββββββ¬ββββββββββββββββββ
β needs: static-security-scans
βΌ
βββββββββββββββββββββββββββββββββββ
β JOB 2 β Build & Verify β
β β
β β Maven compile + package β
β β Trivy FS (SCA scan) β
β β Docker build β
β β Trivy Image (container scan) β
βββββββββββββββββ¬ββββββββββββββββββ
β needs: build-and-verify
βΌ
βββββββββββββββββββββββββββββββββββ
β JOB 3 β Ephemeral AWS Deploy β
β β
β β Two-phase S3 staging β
β β VPC Β· ALB Β· ASG Β· RDS map β
βββββββββββββββββ¬ββββββββββββββββββ
β needs: ephemeral-deploy
βΌ
βββββββββββββββββββββββββββββββββββ
β JOB 4 β OWASP ZAP DAST β
β β
β β ALB health-check polling β
β β OWASP ZAP baseline scan β
ββββββββββββ¬ββββββββββββββ¬βββββββββ
β if: always()β needs: ephemeral-deploy + dast
βΌ βΌ
βββββββββββββββββ βββββββββββββββββββββββββββββββ
β JOB 5 β β JOB 6 β Branch Gate β
β Teardown β β β
β (guaranteed) β β β Atomic compliance status β
βββββββββββββββββ βββββββββββββββββββββββββββββββ
| Tool | Purpose | Failure Behavior |
|---|---|---|
| Trufflehog | Scans full git commit history for verified secrets (API keys, tokens, passwords) | Hard fail β blocks pipeline |
| Semgrep | SAST against p/java + p/owasp-top-ten rulesets |
Hard fail β blocks pipeline |
| Checkov | Terraform IaC compliance (IMDSv2, S3 encryption, security group lockdown) | Hard fail with curated skip_check profile for ephemeral sandbox constraints |
| Tool | Purpose | Failure Behavior |
|---|---|---|
| Maven | Compiles with pinned tomcat.version: 10.1.55, postgresql.version: 42.7.11 and jackson-bom.version: 2.21.4 to block transitive RCE vulnerabilities |
Hard fail |
| Trivy FS | SCA scan of all file system dependencies β zero tolerance for CRITICAL/HIGH CVEs |
Hard fail |
| Trivy Image | Container layer analysis on eclipse-temurin:17-jre-alpine |
Hard fail |
An umbrella job (secure-validation-gate) aggregates the outcome of all upstream jobs into a single atomic status check that GitHub's branch protection rules evaluate before allowing a merge to main.
The pipeline solves a non-trivial distribution problem: GitHub Actions runner nodes are ephemeral VMs with no network path into the private AWS subnets where compute lives. The Two-Phase Staging Bucket Pattern bridges this gap cleanly.
Terraform targets only the S3 deployment bucket (module.compute.aws_s3_bucket.app_deploy in terraform/environments/staging):
- Server-side encryption enabled (
aws:kms) - Public access fully blocked
- 24-hour lifecycle expiry rule (ephemeral data hygiene)
The runner then streams three artifacts into the bucket:
app.jarβ the pre-vetted, pre-compiled Spring Boot binaryDockerfileβ a staging-optimized image built from the JAR, not from sourcedocker-compose.ymlβ the container orchestration spec
Why a separate Dockerfile? The development
Dockerfilerebuilds fromsrc/andpom.xml, which don't exist on the EC2 host. The staging variant usesCOPY app.jardirectly, keeping the image minimal and reproducible.
Terraform applies the full network topology:
Internet
β
βΌ
AWS ALB (Public Subnets, HTTP:80)
β
βΌ
EC2 Auto Scaling Group (Private Subnets)
β user_data bootstrap script:
β 1. Pull app.jar + Dockerfile + docker-compose.yml from S3 (via IAM profile)
β 2. Write /app/.env from Terraform-injected RDS coordinates
β 3. docker compose up
β
βΌ
AWS RDS PostgreSQL 16 (Isolated Subnet Group)
The database password is generated at apply time by Terraform (random_password), published to SSM Parameter Store as a KMS-encrypted SecureString, and injected into the launch template so the boot script can write it into /app/.env β no static credentials in source control, ever. The instance's IAM profile is scoped to read-only access on the artifact bucket only.
Credential-safe by construction. The
.envis written with single-quoted shell echoes and the generated password excludes$and other shell/Compose metacharacters, so the secret survives the shell β.envβ Docker Compose round-trip intact instead of being silently mangled at boot.
Why decouple the database? Running PostgreSQL alongside the Java application inside a
t3.micro(1 GB RAM) instance triggers the Linux OOM killer. Routing connections to managed RDS keeps the compute layer stateless and horizontally scalable.
The Terraform footprint is a reusable module library, not a monolith. Small, single-responsibility, security-hardened modules are composed by thin per-environment roots β each with its own remote state. Adding an environment is configuration, not code duplication.
terraform/
βββ bootstrap/ # One-time backend: state bucket, KMS, GitHub OIDC role
βββ modules/ # Reusable building blocks (the pattern library)
β βββ network/ # VPC Β· 3 subnet tiers Β· routing Β· NAT Β· edge SGs
β βββ iam/ # EC2 instance role + profile (least-privilege S3 read)
β βββ database/ # Encrypted Multi-AZ RDS PostgreSQL 16 + generated secret
β βββ compute/ # ALB Β· Auto Scaling Group Β· ephemeral artifact bucket
β βββ observability/ # VPC flow logs β locked-down, self-expiring S3
βββ environments/ # Composition roots (one state file each)
βββ staging/ # Production-representative; deployed by the pipeline
βββ dev/ # Low-cost sandbox twin (single-AZ, no ASG burst)
| Principle | How it shows up |
|---|---|
| Single responsibility | One concern per module; clean input/output contracts |
| No provider/backend in modules | Environments own state + provider; modules own resources |
| DRY environments | dev and staging share every module; differ only in tfvars |
| Cycle-free wiring | iam β compute decoupled via a shared bucket name string |
Namespaced by project_name |
dev and staging coexist in one account without collisions |
| Compliance built in | Each module ships its own Checkov fixes (IMDSv2, KMS, PAB, β¦) |
Full catalog, dependency graph, and how-to (new module / new environment):
docs/terraform-module-usage.md.
All security measures are implemented in code β no manual server configuration required.
A global jakarta.servlet.Filter (priority @Order(1)) intercepts every outbound HTTP response β regardless of controller, path, or status code β and injects the following headers:
| Header | Value | Fixes |
|---|---|---|
X-Content-Type-Options |
nosniff |
ZAP [10021] β MIME sniffing attacks |
Cross-Origin-Resource-Policy |
same-origin |
ZAP [90004] β Cross-origin resource leakage |
Cache-Control |
no-store |
ZAP [10049] β Sensitive response caching |
X-Frame-Options |
DENY |
Clickjacking / UI redressing |
X-XSS-Protection |
0 |
Disables legacy broken browser XSS filter (OWASP recommendation) |
Unmapped administrative paths that would otherwise trigger Spring Boot's default 500 error page (leaking framework details) are explicitly intercepted and return sanitized JSON responses:
| Path | Behavior | HTTP Status |
|---|---|---|
/actuator/ |
{"error": "Not Found"} β path existence is not confirmed |
404 |
/api/v1/admin/ |
{"error": "Forbidden"} β structured, non-disclosive |
403 |
Framework error signatures are stripped from all client-facing responses:
# Disable Spring Boot white-label error page
server.error.whitelabel.enabled=false
# Strip all framework debug information from error responses
server.error.include-message=never
server.error.include-binding-errors=never
server.error.include-stacktrace=never
server.error.include-exception=falseA @RestControllerAdvice handler produces consistent, OWASP-compliant error payloads for all exception types without leaking stack traces or internal class names:
{
"timestamp": "2026-06-23T14:00:00Z",
"status": 404,
"error": "Not Found",
"message": "Product with id 42 was not found.",
"path": "/api/products/42"
}OWASP ZAP baseline scan results after hardening (production target):
PASS: Loosely Scoped Cookie [90033]
PASS: X-Content-Type-Options [10021] β Fixed by SecurityHeadersFilter
PASS: Information Disclosure [10023] β Fixed by explicit endpoint handlers
PASS: Cross-Origin-Resource-Policy [90004] β Fixed by SecurityHeadersFilter
PASS: Application Error Disclosure [90022] β Fixed by explicit endpoint handlers
IGNORE: Non-Storable Content [10049] β HTTP 500/204 are non-cacheable by spec
FAIL-NEW: 0 WARN-NEW: 0 PASS: 62+
ββββββββββββ 1:1 ββββββββββββ 1:N ββββββββββββββ
β User βββββββββββββββββΊβ Cart ββββββββββββββββΊβ CartItem β
β β β β β β
β id β β id β β id β
β username β β user_id β β cart_id β
β email β ββββββββββββ β product_id β
βcreatedAt β β quantity β
ββββββββββββ βββββββ¬βββββββ
β β
β 1:N β N:1
βΌ βΌ
βββββββββββββ 1:N βββββββββββββ N:1 βββββββββββββββ
β Order ββββββββββββββββΊβ OrderItem βββββββββββββββΊβ Product β
β β β β β β
β id β β id β β id β
β user_id β β order_id β β name β
β status β β product_idβ β description β
βtotalAmountβ β quantity β β price β
β createdAt β βpriceAtPurchase β category β
βββββββββββββ βββββββββββββ βstockQuantityβ
β createdAt β
βββββββββββββββ
Design decisions:
CartItemenforces a unique constraint on(cart_id, product_id)β duplicatePOSTcalls increment quantity rather than creating duplicate rows.OrderItem.priceAtPurchasesnapshots the price at checkout time β product price changes never retroactively affect historical order data.Order.@PrePersistnull-guardscreatedAtto allow theDataSeederto inject realistic historical timestamps for demo data.- All lazy-loaded associations use
JOIN FETCHin repository queries to eliminate N+1 query patterns.
All endpoints return application/json. Endpoints that operate on cart and order data require an X-User-Id header.
GET /api/users/{id} β Retrieve a user profile
curl http://localhost:8080/api/users/1{
"id": 1,
"username": "alice_dev",
"email": "alice.devlin@techcorp.io",
"createdAt": "2026-06-16T17:00:00"
}POST /api/users β Create a new user account
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{ "username": "newuser", "email": "new@example.com" }'GET /api/products β List all products with optional filters
Supports ?category= (exact match) and ?search= (case-insensitive substring).
curl "http://localhost:8080/api/products?category=Fitness&search=yoga"POST /api/products β Create a new product
Validates: price β₯ 0.01, non-blank name, stock quantity β₯ 0.
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{ "name": "Yoga Mat Pro", "price": 49.99, "category": "Fitness", "stockQuantity": 100 }'Requires X-User-Id: {userId} header on all requests.
GET /api/carts β Retrieve the active cart with computed totals
curl http://localhost:8080/api/carts -H "X-User-Id: 1"POST /api/carts/items β Add an item to the cart
Validates stock availability in real time. Duplicate product adds increment quantity.
curl -X POST http://localhost:8080/api/carts/items \
-H "X-User-Id: 1" \
-H "Content-Type: application/json" \
-d '{ "productId": 3, "quantity": 2 }'POST /api/orders/checkout β Atomic checkout transaction
Executes inside a single @Transactional boundary:
- Validates stock levels for every cart item
- Decrements inventory counters atomically
- Snapshots
priceAtPurchasefor each line item - Creates the
Orderrecord with statusPENDING - Clears the user's cart
curl -X POST http://localhost:8080/api/orders/checkout \
-H "X-User-Id: 1"devsecops_project02/
β
βββ .github/workflows/
β βββ devsecops-pipeline.yml # 6-job parallel DAG CI/CD pipeline
β
βββ .zap/
β βββ rules.tsv # OWASP ZAP custom rule overrides
β
βββ docs/ # Self-service, ops & IaC documentation
β βββ developer-self-service-guide.md
β βββ runbook.md
β βββ troubleshooting.md
β βββ terraform-module-usage.md
β
βββ terraform/ # IaC pattern library (modules + environments)
β βββ bootstrap/ # One-time backend: state bucket, KMS, OIDC role
β βββ modules/ # Reusable, single-responsibility building blocks
β β βββ network/ # VPC, subnets, routing, NAT, edge security groups
β β βββ iam/ # EC2 instance role + profile (least-privilege S3)
β β βββ database/ # Encrypted Multi-AZ RDS PostgreSQL + secret
β β βββ compute/ # ALB + Auto Scaling Group + artifact bucket
β β βββ observability/ # VPC flow logs β locked-down S3
β βββ environments/ # Composition roots (one state file each)
β βββ staging/ # Production-representative; deployed by CI
β βββ dev/ # Low-cost sandbox twin
β
βββ Dockerfile # Multi-stage container build (local)
βββ docker-compose.yml # Local development stack
βββ pom.xml # Dependency management with version pins
β
βββ src/main/
βββ java/com/ecommerce/api/
β βββ config/
β β βββ SecurityHeadersFilter.java # Global HTTP security header injector
β β βββ DataSeeder.java # Idempotent demo data seeder
β β
β βββ controller/
β β βββ BaseUtilityController.java # Root, robots.txt, sitemap, safe fallbacks
β β βββ UserController.java
β β βββ ProductController.java
β β βββ CartController.java
β β βββ OrderController.java
β β
β βββ exception/
β β βββ GlobalExceptionHandler.java # Unified OWASP-compliant error responses
β β
β βββ model/ # JPA entities (User, Product, Cart, Orderβ¦)
β βββ repository/ # Spring Data JPA repositories
β βββ service/ # Business logic layer
β
βββ resources/
βββ application.properties # Hardened production configuration
Operational and self-service documentation lives in docs/:
| Guide | For | Covers |
|---|---|---|
| Developer Self-Service | Any developer | Local loop, standing up your own dev sandbox, the paved road to staging, changing infra safely |
| Terraform Module Usage | Anyone touching IaC | Module catalog, dependency graph, adding a new module/environment |
| Runbook | On-call / operators | Deploy, promote, roll back, tear down, secret rotation, on-call triage |
| Troubleshooting | Everyone | Symptom β cause β fix across IaC, runtime, and pipeline gates |
| Requirement | Minimum Version |
|---|---|
| JDK | 17 (Eclipse Temurin recommended) |
| Maven | 3.9+ |
| Docker Engine | 24+ with Compose Plugin |
# Start PostgreSQL 16 + Spring Boot API in isolated containers
docker compose up -d --build
# Stream live application logs
docker compose logs -f api
# Stop and remove containers
docker compose down# Run the full test suite
mvn test
# Compile and package the JAR (skip tests)
mvn clean package -DskipTests=trueBuilt with security-first principles Β· Designed for automated validation pipelines