A two-tier resume submission portal - Nginx frontend + Flask backend - packaged as containers, secured through a DevSecOps CI pipeline, and deployed to Kubernetes on AWS EKS. Images are built and scanned in GitHub Actions and pushed to ECR using GitHub OIDC (no long-lived AWS keys). Deployment to the cluster is done manually, by hand, as a learning exercise in Kubernetes objects.
This repo is intentionally infrastructure-light: no Terraform. The AWS prerequisites (OIDC role, ECR) are a small one-time bootstrap, and the EKS cluster is assumed to exist. The focus is a solid CI security pipeline + understanding k8s objects.
flowchart TB
user([User browser])
subgraph eks["EKS cluster (namespace: rp-portal)"]
fsvc["Service: rp-portal-frontend :80"]
subgraph fe["Frontend Deployment - Nginx"]
direction LR
fp1["Pod 1"] ~~~ fp2["Pod 2"]
end
bsvc["Service: rp-portal-backend :80"]
subgraph be["Backend Deployment - Flask"]
direction LR
bp1["Pod 1"] ~~~ bp2["Pod 2"] ~~~ bp3["Pod 3"] ~~~ bp4["Pod 4"]
end
end
subgraph aws["AWS managed services (outside the cluster)"]
direction LR
rds[("RDS PostgreSQL")]
s3[("S3 resume PDFs")]
ses["SES email"]
rds ~~~ s3 ~~~ ses
end
user --> fsvc
fsvc --> fe
fe --> bsvc
bsvc --> be
be --> aws
classDef store fill:#f5f0ff,stroke:#7c5cff,color:#000;
class rds,s3,ses store;
Traffic flows one way through the cluster: the frontend Service load-balances across the 2 frontend pods (Nginx), which call the backend Service, which load-balances across the 4 backend pods (Flask). The backend pods reach RDS, S3, and SES using an IRSA-scoped IAM role, so no static credentials live in the pod.
| Layer | Technology |
|---|---|
| Frontend | HTML/CSS/JS served by Nginx |
| Backend | Python Flask + Gunicorn |
| Database | PostgreSQL on AWS RDS |
| Orchestration | Kubernetes on AWS EKS |
| Registry | AWS ECR (image scan + cosign signature) |
| CI / DevSecOps | GitHub Actions |
| CI → AWS auth | GitHub OIDC (no stored keys) |
| Pod → AWS auth | IRSA |
rp-portal/
├── backend/ # Flask backend (app.py, Dockerfile, requirements.txt)
├── frontend/ # Nginx frontend (index.html, nginx.conf, Dockerfile)
├── k8s/
│ ├── namespace.yaml # rp-portal namespace (apply first, never use default)
│ ├── frontend/frontend.yaml # simple, commented manifest (start here)
│ ├── flask/ # backend manifests (deployment, service, hpa, config, secret)
│ ├── nginx/ # original frontend manifests
│ └── ingress.yaml # ALB ingress rules
├── .github/workflows/ci.yml # DevSecOps pipeline (both images)
└── docs/
├── deploy-frontend-to-eks.md # manual build → ECR → deploy walkthrough
└── devsecops-pipeline.md # pipeline stages + one-time AWS bootstrap
Workflow: .github/workflows/ci.yml · Full write-up:
docs/devsecops-pipeline.md
Stage 1 - source security gates (every PR and push):
| Check | Tool | Blocks build? |
|---|---|---|
| Secret scan | gitleaks | Yes |
| SAST | Bandit, Semgrep | Reports (soft) |
| Dependency + IaC scan | Trivy fs | Yes on CRITICAL |
| Dockerfile lint | hadolint | Yes on errors |
Stage 2 - per image (backend + frontend), only after Stage 1 passes:
build → Trivy image scan (blocks on HIGH/CRITICAL) → push to ECR → SBOM (CycloneDX) → cosign keyless signature
- OIDC, no keys - the only GitHub secret is
AWS_ROLE_ARN. - PRs scan but never push - a vulnerable branch can't publish an image.
- Images publish as
rp-portal-backendandrp-portal-frontend.
Findings appear under GitHub → Security → Code scanning (SARIF).
Create the OIDC provider, the rp-portal-gha IAM role, and the AWS_ROLE_ARN GitHub
secret. Copy-paste commands: docs/devsecops-pipeline.md.
Push to main (or run the workflow manually) - CI scans and pushes both images to ECR.
To do it by hand instead, follow docs/deploy-frontend-to-eks.md.
Start with the frontend - the simplest object set - using the commented manifest k8s/frontend/frontend.yaml and the step-by-step guide docs/deploy-frontend-to-eks.md.
| Practice | How |
|---|---|
| No long-lived CI keys | GitHub OIDC → short-lived role session |
| No static pod credentials | IRSA least-privilege role |
| Secrets never committed | gitleaks gate in CI |
| Known-vuln images blocked | Trivy image scan gate (HIGH/CRITICAL) |
| Vulnerable deps blocked | Trivy fs gate (CRITICAL) |
| Supply-chain provenance | SBOM + cosign keyless signatures |
| Private database | RDS in private subnet |
| Container hardening | Non-root user in backend image |
| Namespace isolation | App runs in the rp-portal namespace, never default |