DevSecMovie integrates security directly into the software development lifecycle — shifting security left rather than treating it as an afterthought. Every code push triggers an automated pipeline that runs secret detection, static analysis, dependency scanning, container scanning, and infrastructure scanning before deploying the application.
The base application is a movie and TV series discovery platform powered by the TMDB API, built with React, TypeScript, and Tailwind CSS. It supports search, trailers, and browsing by category.
What makes this a DevSecOps project:
- Secrets are never hardcoded — all sensitive values are stored in Jenkins credentials and injected at build time
- Security scanning happens at every layer: source code, dependencies, Docker image, and Terraform infrastructure
- The pipeline fails fast — if a critical vulnerability is detected, the deployment is blocked
- All scan reports are archived as Jenkins artifacts for audit trails
- Infrastructure is fully reproducible via Terraform — no manual console clicks
A production-grade DevSecOps project built on top of a React + TypeScript movie discovery app. This project demonstrates a complete CI/CD pipeline with security scanning at every stage, deployed to AWS using infrastructure-as-code.
Live App:
http://34.206.161.111Pipeline: Jenkins CI/CD with 11 automated security stages Infrastructure: Terraform-provisioned AWS EC2, ECR, VPC
- Project Overview
- Architecture
- Tech Stack
- Pipeline Stages
- Infrastructure
- Project Structure
- Getting Started
- Environment Variables
- Docker
- Jenkins Setup
- Security Tools
- Monitoring
- Cost Optimisation
- Lessons Learned
Developer (local)
│
│ git push
▼
GitHub (source of truth)
│
│ webhook / manual trigger
▼
┌─────────────────────────────────────────────┐
│ Jenkins CI/CD Server │
│ EC2 t2.large (98.88.241.84) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │SonarQube│ │Prometheus│ │ Grafana │ │
│ │:9000 │ │:9090 │ │:3000 │ │
│ └─────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
│
│ docker push
▼
AWS ECR (container registry)
│
│ docker pull + run
▼
┌─────────────────────────────┐
│ App Server │
│ EC2 t2.medium │
│ (34.206.161.111) │
│ nginx:80 → React SPA │
└─────────────────────────────┘
Both EC2 instances live inside a custom VPC with public subnets, internet gateway, and security groups that follow least-privilege principles.
| Layer | Technology |
|---|---|
| Frontend framework | React 18 + TypeScript |
| Styling | Tailwind CSS |
| Build tool | Vite |
| API | TMDB (The Movie Database) |
| Web server | nginx (alpine) |
| Container | Docker (multi-stage build) |
| Category | Tool | Purpose |
|---|---|---|
| CI/CD | Jenkins 2.541 | Pipeline orchestration |
| Secret scanning | GitLeaks 8.18 | Detect hardcoded secrets in git history |
| SAST | SonarQube 9.9 LTS | Static code analysis, quality gate |
| Dependency scanning | OWASP Dependency-Check | CVE scanning of npm packages |
| Filesystem scanning | Trivy 0.69 | Scan source files for vulnerabilities |
| Image scanning | Trivy 0.69 | Scan Docker image layers for CVEs |
| IaC scanning | Checkov 3.2 | Terraform misconfiguration detection |
| Container registry | AWS ECR | Private Docker image storage |
| Infrastructure | Terraform 1.6 | Infrastructure as Code |
| Monitoring | Prometheus + Grafana | Metrics and dashboards |
| Service | Usage |
|---|---|
| EC2 (t2.large) | Jenkins server |
| EC2 (t2.medium) | Application server |
| ECR | Docker image registry |
| VPC | Network isolation |
| Security Groups | Firewall rules |
| Elastic IPs | Static public IPs |
| IAM | Instance profiles for ECR access |
| S3 | Terraform remote state |
| DynamoDB | Terraform state locking |
The Jenkins pipeline consists of 11 stages that run automatically on every build:
Wipes the Jenkins workspace before each build to ensure a clean slate and prevent stale file contamination between builds.
Clones the repository from GitHub using the branch specified in the pipeline configuration.
Runs GitLeaks against the entire git history (all commits) to detect hardcoded secrets such as API keys, tokens, passwords, and private keys. Reports are archived as JSON artifacts.
Tool: GitLeaks 8.18.2
Scope: All 69 commits in history
Output: gitleaks-report.json
Performs static application security testing on the TypeScript source code. Analyses 45 source files for bugs, vulnerabilities, security hotspots, and code smells. Results are sent to the SonarQube server running on the same EC2 instance.
Tool: SonarQube 9.9 LTS
Files analysed: 45 TypeScript/CSS files
Quality Gate: Passed
Result: 0 bugs, 0 vulnerabilities, 0 security hotspots, 21 code smells
Waits for SonarQube to complete processing the analysis report and checks whether the quality gate conditions are met. Uses a SonarQube webhook to avoid polling. Pipeline proceeds only if the gate passes.
Runs npm ci --frozen-lockfile to install exact versions from package-lock.json. This ensures reproducible builds and surfaces any installation issues early.
Scans all npm dependencies against the NVD (National Vulnerability Database) for known CVEs. Generates both HTML and XML reports archived as Jenkins artifacts.
Tool: OWASP Dependency-Check (latest)
Database: NVD CVE database (342,280 records)
Output: dependency-check-report.html, dependency-check-report.xml
Scans the source code filesystem for HIGH and CRITICAL severity vulnerabilities before building the Docker image. This catches issues in the codebase before containerisation.
Tool: Trivy 0.69.3
Severity filter: HIGH, CRITICAL
Output: trivy-fs-report.txt
Builds the production Docker image using a multi-stage Dockerfile. Stage 1 uses node:20-alpine to compile the React app with Vite. Stage 2 copies the compiled dist/ folder into nginx:1.27-alpine. The TMDB API key is injected as a build argument and baked into the JavaScript bundle at compile time.
Base images: node:20-alpine (builder), nginx:1.27-alpine (final)
Build args: VITE_API_KEY, VITE_TMDB_API_BASE_URL
Final image size: ~50MB
Scans the built Docker image for HIGH and CRITICAL CVEs across all layers including the OS packages, language runtimes, and application dependencies.
Tool: Trivy 0.69.3
Target: ECR image (build number tag)
Severity filter: HIGH, CRITICAL
Output: trivy-image-report.txt
Scans all Terraform files in the terraform/ directory for security misconfigurations and compliance violations. Uses --soft-fail so findings are reported but do not block deployment — allowing teams to review and remediate iteratively.
Tool: Checkov 3.2.513
Target: terraform/ directory
Mode: --soft-fail (report, don't block)
Output: checkov-report.txt
Authenticates with AWS ECR using the EC2 instance's IAM role (no static credentials required), then pushes the image with both a build number tag and latest tag.
Registry: 757760338065.dkr.ecr.us-east-1.amazonaws.com/devsecmovie
Tags: :<build_number>, :latest
Auth: IAM instance profile (no stored credentials)
SSHs into the app server using a stored Jenkins SSH credential, pulls the latest image from ECR, stops and removes the old container, and starts a new one on port 80.
docker pull $ECR_REPO:latest
docker stop devsecmovie
docker rm devsecmovie
docker run -d --name devsecmovie --restart unless-stopped -p 80:80 $ECR_REPO:latestAll infrastructure is provisioned and managed by Terraform. No manual AWS Console configuration is required after initial setup.
VPC
├── Public subnet (10.0.1.0/24)
├── Internet Gateway
└── Route table
EC2: jenkins-server (t2.large)
├── Security group (ports: 22, 8080, 9000, 3000, 9090)
├── Elastic IP
├── IAM instance profile (ECR access)
├── 30GB gp3 EBS volume
└── User data script (auto-installs all tools on boot)
EC2: app-server (t2.medium)
├── Security group (ports: 22, 80, 443)
├── Elastic IP
├── IAM instance profile (ECR access)
├── 15GB gp3 EBS volume
└── User data script (installs Docker + AWS CLI)
ECR: devsecmovie
├── Image scanning on push enabled
└── Lifecycle policy (keep last 10 images)
S3: devsecmovie-tfstate (remote state)
DynamoDB: devsecmovie-tflock (state locking)
# Bootstrap (run once)
aws s3 mb s3://devsecmovie-tfstate --region us-east-1
aws dynamodb create-table \
--table-name devsecmovie-tflock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region us-east-1
# Deploy infrastructure
terraform init
terraform plan
terraform apply
# Destroy when done
terraform destroyDevSecMovieApp/
├── src/ # React + TypeScript source
│ ├── components/
│ ├── pages/
│ ├── hooks/
│ └── utils/
│ └── config.ts # Environment variable exports
├── Dockerfile # Multi-stage production build
├── nginx.conf # nginx SPA routing + security headers
├── docker-compose.yml # Local development
├── Jenkinsfile # 11-stage DevSecOps pipeline
├── .gitignore
└── terraform/
├── main.tf # Provider + S3 backend
├── variables.tf
├── vpc.tf # VPC, subnet, IGW, routes
├── sg.tf # Security groups
├── iam.tf # EC2 instance profiles
├── ecr.tf # Container registry
├── ec2.tf # Jenkins + App servers
├── outputs.tf # IPs, URLs, ECR URL
├── terraform.tfvars # Your values (gitignored)
└── scripts/
├── jenkins_install.sh # Bootstrap: Java, Jenkins, Docker, Trivy, etc.
└── app_install.sh # Bootstrap: Docker, AWS CLI
- AWS account with programmatic access (Access Key + Secret Key)
- Terraform >= 1.6 installed locally
- AWS CLI v2 configured (
aws configure) - Docker Desktop installed locally
- Node.js 18+ installed locally
- TMDB API key (free at themoviedb.org)
# Clone the repo
git clone https://github.com/Emmsfay/DevSecMovieApp.git
cd DevSecMovieApp
# Install dependencies
npm install
# Create .env file
echo "VITE_API_KEY=your_tmdb_api_key" > .env
echo "VITE_TMDB_API_BASE_URL=https://api.themoviedb.org/3" >> .env
# Start dev server
npm run dev
# App runs at http://localhost:5173docker build \
--build-arg VITE_API_KEY=your_tmdb_api_key \
--build-arg VITE_TMDB_API_BASE_URL=https://api.themoviedb.org/3 \
-t devsecmovie:local .
docker run -d --name devsecmovie -p 8080:80 devsecmovie:local
# App runs at http://localhost:8080# 1. Create bootstrap resources
aws s3 mb s3://devsecmovie-tfstate --region us-east-1
# 2. Configure variables
cd terraform
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your key_name and my_ip
# 3. Provision infrastructure
terraform init
terraform plan
terraform apply
# 4. Note the outputs
# jenkins_url, sonarqube_url, app_url, ecr_repository_url
# 5. Configure Jenkins (see Jenkins Setup section)
# 6. Create pipeline pointing to this repo
# 7. Click Build Now| Variable | Description | Required |
|---|---|---|
VITE_API_KEY |
TMDB API key (v3 auth) | Yes |
VITE_TMDB_API_BASE_URL |
TMDB base URL | Yes |
VITE_GA_MEASUREMENT_ID |
Google Analytics ID | No |
VITE_GOOGLE_AD_SLOT |
Google Ad slot | No |
VITE_GOOGLE_AD_CLIENT |
Google Ad client | No |
The VITE_ prefix is required by Vite to expose variables to the browser bundle. Variables without this prefix are not accessible in the frontend code.
Important: Never commit .env files to git. The .gitignore excludes them. In the pipeline, values are stored as Jenkins credentials and injected at build time.
The Dockerfile uses a multi-stage build to keep the final image small and secure:
Stage 1 (builder): Uses node:20-alpine to install dependencies and compile the React app. The TMDB API key is baked into the JavaScript bundle during this stage via Vite's environment variable system.
Stage 2 (final): Uses nginx:1.27-alpine — a minimal web server image. Only the compiled dist/ folder is copied from Stage 1. No Node.js, no npm, no source code exists in the final image.
Security headers are configured in nginx.conf:
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Static asset caching is set to 1 year for all hashed filenames (Vite fingerprints all assets).
| Plugin | Purpose |
|---|---|
| Eclipse Temurin Installer | JDK 21 management |
| NodeJS | Node 18 in pipeline |
| SonarQube Scanner | SAST integration |
| OWASP Dependency-Check | CVE scanning |
| Pipeline AWS Steps | ECR authentication |
| SSH Agent | Deploy to app server |
| Docker Pipeline | Docker build steps |
| Prometheus Metrics | Expose metrics for Grafana |
| ID | Kind | Value |
|---|---|---|
Sonar-token |
Secret text | SonarQube user token |
TMDB_API_KEY |
Secret text | TMDB API key |
ECR_REPO |
Secret text | Full ECR repository URL |
APP_SERVER_IP |
Secret text | App server public IP |
app-server-ssh-key |
SSH username + private key | EC2 key pair (.pem contents) |
SonarQube runs as a Docker container on the Jenkins server:
docker run -d \
--name sonarqube \
--restart unless-stopped \
-p 9000:9000 \
sonarqube:lts-communityAccess at http://<jenkins_ip>:9000 — default credentials: admin/admin.
A webhook must be configured in SonarQube pointing to Jenkins:
Name: jenkins
URL: http://<jenkins_ip>:8080/sonarqube-webhook/
This allows the Quality Gate stage to complete quickly instead of polling.
Scans git history for secrets using pattern matching and entropy analysis. Detects API keys, tokens, passwords, private keys, and connection strings. Configured with --exit-code 0 so findings are reported but don't fail the build — allowing teams to review and rotate secrets without blocking deployment.
Performs deep static analysis of TypeScript source code. The Quality Gate checks:
- 0 new bugs
- 0 new vulnerabilities
- 0 new security hotspots unreviewed
- Code coverage threshold
Results are visible at http://<jenkins_ip>:9000/dashboard?id=DevSecMovie.
Cross-references all npm packages against the NVD CVE database. Particularly effective at catching known vulnerabilities in third-party libraries that developers may have missed. The NVD database is downloaded on first run (~342,000 records) and cached for subsequent runs.
Aqua Security's open-source vulnerability scanner. Used at two points in the pipeline:
- Filesystem scan — catches vulnerabilities before building
- Image scan — catches vulnerabilities in the final container layers including OS packages
The nginx:1.27-alpine base image is chosen for minimal attack surface.
Scans Terraform infrastructure code for security misconfigurations against hundreds of built-in policies. Checks for issues like:
- Overly permissive security groups
- Unencrypted storage
- Missing logging
- Public access misconfigurations
- IAM policy violations
Prometheus and Grafana run as Docker containers on the Jenkins server.
Prometheus scrapes metrics from:
- Jenkins (via Prometheus metrics plugin at
/prometheus) - Node Exporter (system metrics: CPU, RAM, disk, network)
Grafana dashboards:
- Jenkins performance dashboard (ID:
9964) — build duration, queue depth, success rate - Node Exporter full dashboard (ID:
1860) — system resources
Access Grafana at http://<jenkins_ip>:3000 — default credentials: admin/admin.
This project is designed for learning and portfolio purposes. Stop instances when not in use to avoid unnecessary charges.
Estimated cost (running 8 hours/day):
| Resource | Monthly cost |
|---|---|
| Jenkins EC2 (t2.large) | ~$12–15 |
| App EC2 (t2.medium) | ~$6–8 |
| ECR storage | ~$0.10 |
| Elastic IPs (attached) | $0 |
| S3 remote state | ~$0.01 |
| Total | ~$18–24 |
Stop instances when done:
aws ec2 stop-instances \
--instance-ids \
$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=devsecmovie-jenkins" \
--query "Reservations[0].Instances[0].InstanceId" \
--output text --region us-east-1) \
$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=devsecmovie-app" \
--query "Reservations[0].Instances[0].InstanceId" \
--output text --region us-east-1) \
--region us-east-1Start instances again:
aws ec2 start-instances \
--instance-ids \
$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=devsecmovie-jenkins" \
--query "Reservations[0].Instances[0].InstanceId" \
--output text --region us-east-1) \
$(aws ec2 describe-instances \
--filters "Name=tag:Name,Values=devsecmovie-app" \
--query "Reservations[0].Instances[0].InstanceId" \
--output text --region us-east-1) \
--region us-east-1Note: Elastic IPs are free while attached to running instances but incur a small charge (~$0.005/hour) when the instance is stopped. For longer breaks, consider releasing the EIPs and using terraform apply to reassign new ones when resuming.
Jenkins GPG key rotation — The Jenkins Debian repository rotated its GPG key from jenkins.io-2023.key to jenkins.io-2026.key. Bootstrap scripts must use the latest key URL or installation fails silently.
Vite environment variables — Vite requires all browser-accessible variables to be prefixed with VITE_. Variables are baked into the JavaScript bundle at build time, not injected at runtime. Both the variable name in the Dockerfile and the --build-arg in the Jenkinsfile must match exactly.
Multi-stage Docker builds — The API key only exists in the builder stage. The final nginx image contains only compiled static files — making it smaller, faster, and more secure.
Git history hygiene — Large binaries committed to git (such as Terraform provider plugins) bloat the repository significantly. The .gitignore must exclude terraform/.terraform/ before the first commit. Use git filter-repo to remove large files from history.
Checkov PATH issues — Tools installed via pipx install into user-specific directories not accessible by service accounts like jenkins. The binary must be copied (not symlinked) to a system-wide location with world-executable permissions, and the shebang must point to a Python interpreter accessible by all users.
Dynamic IPs in AWS — EC2 instances get new public IPs on stop/start unless an Elastic IP is attached. Security group rules that restrict SSH by IP need updating when the client's ISP assigns a new address. For development, using 0.0.0.0/0 for SSH is acceptable but should never be done in production.
SonarQube quality gate webhook — Without a webhook, the Jenkins quality gate stage polls SonarQube repeatedly and can time out. Adding a webhook at Administration → Webhooks pointing to http://<jenkins_ip>:8080/sonarqube-webhook/ makes the gate complete in seconds.
- Base application: sudeepmahato16/movie-app



