diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..615bd34 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Exclude non-essential files from Docker build context +# This speeds up builds and reduces the attack surface + +# Kubernetes/Helm/GitOps (not needed in image) +charts/ +gitops/ +scripts/ + +# Documentation and screenshots +screenshots/ +*.md +HELP.md + +# CI/CD configs +.github/ + +# Build output (will be rebuilt inside Docker) +target/ + +# IDE and OS files +.idea/ +.vscode/ +*.iml +*.iws +.git/ + +# Local dev files +docker-compose.yml +app-tier.yml +.env diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4801575..361ddc7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: workflow_call: permissions: - id-token: write # Required for OIDC contents: read jobs: @@ -77,31 +76,27 @@ jobs: path: trivy-report.txt # ============================================================ - # GATE 7: PUSH TO ECR + # GATE 7: PUSH TO DOCKER HUB # ============================================================ - push_to_ecr: - name: Push to ECR + push_to_dockerhub: + name: Push to Docker Hub runs-on: ubuntu-latest needs: image_scan steps: - name: Checkout Code uses: actions/checkout@v4 - - name: Configure AWS Credentials (OIDC) - uses: aws-actions/configure-aws-credentials@v4 + - name: Login to Docker Hub + uses: docker/login-action@v3 with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build, Tag, and Push Image env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} + DOCKERHUB_REPO: ${{ secrets.DOCKERHUB_REPO }} IMAGE_TAG: ${{ github.sha }} run: | - docker build --pull -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + docker build --pull -t $DOCKERHUB_REPO:$IMAGE_TAG . + docker push $DOCKERHUB_REPO:$IMAGE_TAG + diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index af8419e..c0af8dd 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,94 +1,34 @@ -name: CD - Deploy & DAST - +name: CD - GitOps Sync + on: workflow_call: -permissions: - id-token: write # Required for OIDC - contents: read - jobs: - # ============================================================ - # GATE 8: DEPLOY + # GATE 8: GITOPS UPDATE # ============================================================ - deploy: - name: Deploy to EC2 via SSH + gitops-update: + name: Update Helm Values for ArgoCD runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 - - - name: Create Deployment Directory - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: mkdir -p ~/bankapp - - - name: Copy app-tier.yml to EC2 - uses: appleboy/scp-action@master - with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - source: "app-tier.yml" - target: "~/bankapp" - - - name: Deploy via SSH - uses: appleboy/ssh-action@master with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USER }} - key: ${{ secrets.EC2_SSH_KEY }} - script: | - # Login to ECR - aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com + token: ${{ secrets.GITHUB_TOKEN }} # contents: write permission is set above — no PAT needed - cd ~/bankapp + - name: Update Image Tag in Helm values.yaml + run: | + sed -i 's/tag: .*/tag: "${{ github.sha }}"/' charts/bankapp/values.yaml - # Fetch secrets from Secrets Manager and create .env - aws secretsmanager get-secret-value --secret-id bankapp/prod-secrets --query SecretString --output text > secrets.json - - # Extract variables and write to .env - echo "DB_HOST=$(jq -r .DB_HOST secrets.json)" > .env - echo "DB_PORT=$(jq -r .DB_PORT secrets.json)" >> .env - echo "DB_NAME=$(jq -r .DB_NAME secrets.json)" >> .env - echo "DB_USER=$(jq -r .DB_USER secrets.json)" >> .env - echo "DB_PASSWORD=$(jq -r .DB_PASSWORD secrets.json)" >> .env - echo "OLLAMA_URL=$(jq -r .OLLAMA_URL secrets.json)" >> .env - echo "ECR_REGISTRY=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com" >> .env - echo "ECR_REPOSITORY=${{ secrets.ECR_REPOSITORY }}" >> .env - echo "IMAGE_TAG=${{ github.sha }}" >> .env - - # Clean up - rm secrets.json - - # Pull and Deploy using the app-tier.yml transferred via SCP - docker compose -f app-tier.yml pull - docker compose -f app-tier.yml up -d --build + - name: Commit and Push Changes + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git add charts/bankapp/values.yaml + git commit -m "chore: update image tag to ${{ github.sha }} [skip ci]" + git push # ============================================================ - # GATE 9: DAST - Dynamic Application Security Testing + # GATE 9: ARGOCD SYNC (Optional, ArgoCD can auto-sync) # ============================================================ - dast: - name: DAST - OWASP ZAP Baseline Scan - runs-on: ubuntu-latest - needs: deploy - steps: - - name: Run OWASP ZAP Baseline Scan - uses: zaproxy/action-baseline@v0.15.0 - continue-on-error: true # Workaround for ZAP's internal artifact upload bug - with: - target: 'http://${{ secrets.EC2_HOST }}:8080' - artifact_name: zapreport - fail_action: false - allow_issue_writing: false - - - name: Upload ZAP Report - uses: actions/upload-artifact@v4 - if: always() - with: - name: zap-dast-report - path: report_html.html \ No newline at end of file + # Note: ArgoCD is configured to auto-sync in its manifest \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5955a8c..1dcd4ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,4 +105,4 @@ jobs: if: always() with: name: owasp-dependency-check-report - path: target/dependency-check-report.html + path: target/dependency-check-report.html \ No newline at end of file diff --git a/.github/workflows/devsecops-main.yml b/.github/workflows/devsecops-main.yml index a20e7b8..94693b4 100644 --- a/.github/workflows/devsecops-main.yml +++ b/.github/workflows/devsecops-main.yml @@ -2,7 +2,7 @@ name: DevSecOps Main Pipeline on: push: - branches: [ main, devsecops, k8s ] + branches: [ main ] paths-ignore: - '**.md' - '.gitignore' @@ -11,8 +11,7 @@ on: - 'scripts/**' permissions: - id-token: write - contents: read + contents: write # Required: cd.yml pushes updated values.yaml back to repo (workflow_call inherits this) security-events: write jobs: @@ -24,7 +23,7 @@ jobs: secrets: inherit # ============================================================ - # STAGE 2: Build - Maven + Trivy + ECR Push + # STAGE 2: Build - Maven + Trivy + Docker Hub Push # ============================================================ build: needs: ci @@ -32,7 +31,7 @@ jobs: secrets: inherit # ============================================================ - # STAGE 3: CD - Deploy + DAST + # STAGE 3: CD - GitOps Sync (ArgoCD) # ============================================================ cd: needs: build diff --git a/README.md b/README.md index 0b0c464..550b81c 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ # DevSecOps Banking Application -A high-performance, containerized financial platform built with Spring Boot 3, Java 21, and integrated Contextual AI. This project implements a secure "DevSecOps Pipeline" using GitHub Actions, OIDC authentication, and AWS managed services. +A high-performance, cloud-native financial platform built with Spring Boot 3 and Java 21. Deployed on **Kind (Kubernetes in Docker)** with a fully automated **GitOps pipeline** using GitHub Actions, ArgoCD, and Helm - enforcing **8 sequential security gates** before any code reaches production. [![Java Version](https://img.shields.io/badge/Java-21-blue.svg)](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html) [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4.1-brightgreen.svg)](https://spring.io/projects/spring-boot) -[![GitHub Actions](https://img.shields.io/badge/CI%2FCD-GitHub%20Actions-orange.svg)](.github/workflows/devsecops.yml) -[![AWS OIDC](https://img.shields.io/badge/Security-OIDC-red.svg)](#phase-3-security-and-identity-configuration) +[![GitHub Actions](https://img.shields.io/badge/CI%2FCD-GitHub%20Actions-orange.svg)](.github/workflows/devsecops-main.yml) +[![Kind](https://img.shields.io/badge/Deploy-Kind-yellow.svg)](#phase-3-cloud-native-deployment-gitops--gateway-api) +[![ArgoCD](https://img.shields.io/badge/GitOps-ArgoCD-red.svg)](#phase-3-cloud-native-deployment-gitops--gateway-api) @@ -17,310 +18,436 @@ A high-performance, containerized financial platform built with Spring Boot 3, J ## Technical Architecture -The application is deployed across a multi-tier, segmented AWS environment. The control plane leverages GitHub Actions with integrated security gates at every stage. - -```mermaid -graph TD - subgraph "External Control Plane" - GH[GitHub Actions] - User[User Browser] - end - - subgraph "AWS Infrastructure (VPC)" - subgraph "Application Tier" - AppEC2[App EC2 - Ubuntu/Docker] - DB[(MySQL 8.0 Container)] - end - - subgraph "Artificial Intelligence Tier" - Ollama[Ollama EC2 - AI Engine] - end - - subgraph "Identity & Secrets" - Secrets[AWS Secrets Manager] - OIDC[IAM OIDC Provider] - end - - subgraph "Registry" - ECR[Amazon ECR] - end - end - - GH -->|1. OIDC Authentication| OIDC - GH -->|2. Push Scanned Image| ECR - GH -->|3. SSH Orchestration| AppEC2 - GH -->|4. DAST Scan| AppEC2 - - User -->|Port 8080| AppEC2 - AppEC2 -->|JDBC Connection| DB - AppEC2 -->|REST Integration| Ollama - AppEC2 -->|Runtime Secrets| Secrets - AppEC2 -->|Pull Image| ECR -``` +The application is deployed on a modern, cloud-native **Kind** cluster. GitHub Actions handles all CI/CD security gates, then updates Helm manifests in the repo, which **ArgoCD** automatically synchronizes to the cluster. + +![architecture](screenshots/architecture.png) --- -## Security Pipeline (DevSecOps Pipeline) +## Security Pipeline — 8 Gates + +The pipeline enforces **8 sequential security gates** across three modular workflows: [`ci.yml`](.github/workflows/ci.yml), [`build.yml`](.github/workflows/build.yml), and [`cd.yml`](.github/workflows/cd.yml), all orchestrated by [`devsecops-main.yml`](.github/workflows/devsecops-main.yml). -The CI/CD pipeline enforces **9 sequential security gates** before any code reaches production: +| Gate | Job | Workflow | Tool | Behavior | +| :---: | :--- | :--- | :--- | :--- | +| 1 | `gitleaks` | `ci.yml` | Gitleaks | **Strict** — Fails if any secrets found in full Git history | +| 2 | `lint` | `ci.yml` | Checkstyle | **Audit** — Reports Java style violations (Google Style), does not block | +| 3 | `sast` | `ci.yml` | Semgrep | **Strict** — SAST on Java code (OWASP Top 10 + secrets rules) | +| 4 | `sca` | `ci.yml` | OWASP Dependency Check | **Strict** — Fails if any CVE with CVSS ≥ 7.0 found in Maven deps | +| 5 | `build` | `build.yml` | Maven | Compiles, packages JAR, uploads as build artifact | +| 6 | `image_scan` | `build.yml` | Trivy | **Strict** — Fails on CRITICAL or HIGH CVE in the Docker image | +| 7 | `push_to_dockerhub` | `build.yml` | Docker Hub (Secrets) | Pushes verified image to Docker Hub using secure secrets | +| 8 | `gitops-update` | `cd.yml` | Helm / ArgoCD | Updates `charts/bankapp/values.yaml` with new image tag → triggers ArgoCD auto-sync | -| Gate | Name | Tool | Purpose | -| :---: | :--- | :--- | :--- | -| 1 | Secret Scan | Gitleaks | Scans entire Git history for leaked secrets | -| 2 | Lint | Checkstyle | Enforces Java Google-Style coding standards | -| 3 | SAST | Semgrep | Scans Java source code for security flaws and OWASP Top 10 | -| 4 | SCA | OWASP Dependency Check (first time run can take more than 30+ minutes) | Scans Maven dependencies for known CVEs | -| 5 | Build | Maven | Compiles and packages the application | -| 6 | Container Scan | Trivy | Scans the Docker image for OS and library vulnerabilities | -| 7 | Push | Amazon ECR | Pushes the image only after Trivy passes | -| 8 | Deploy | SSH / Docker Compose | Automated deployment to AWS EC2 | -| 9 | DAST | OWASP ZAP | Dynamic attack surface scanning on live app | +> **ArgoCD** is configured with `automated.selfHeal: true` — once `values.yaml` is updated, ArgoCD automatically pulls and deploys the new image to the Kind cluster without any manual intervention. + +All scan reports (OWASP, Trivy) are uploaded as downloadable **Artifacts** in each GitHub Actions run. --- ## Technology Stack -- **Backend Framework**: Java 21, Spring Boot 3.4.1 -- **Security Strategy**: Spring Security, IAM OIDC, Secrets Manager -- **Persistence Layer**: MySQL 8.0 (Docker Container) -- **AI Integration**: Ollama (TinyLlama) -- **DevOps Tooling**: Docker, Docker Compose, GitHub Actions, AWS CLI, jq -- **Infrastructure**: Amazon EC2, Amazon ECR, Amazon VPC +| Category | Technology | +| :--- | :--- | +| **Backend** | Java 21, Spring Boot 3.4.1, Spring Security, Spring Data JPA | +| **Frontend** | Thymeleaf, Bootstrap | +| **AI Integration** | Google Gemini API (`gemini-3-flash-preview`) | +| **Database** | MySQL 8.0 (Kubernetes Pod) | +| **Container** | Docker (eclipse-temurin:21-jre-alpine, non-root user) | +| **Kubernetes** | Kind (Kubernetes in Docker) | +| **GitOps** | ArgoCD, Helm 3 | +| **Networking** | Kubernetes Gateway API (GatewayClass: `envoy`) | +| **CI/CD** | GitHub Actions (Standard Workflow) | +| **Security Tools** | Gitleaks, Checkstyle, Semgrep, OWASP Dependency Check, Trivy | +| **Registry** | Docker Hub | +| **Secrets** | Kubernetes Secrets, GitHub Actions Secrets | --- ## Implementation Phases -### Phase 1: AWS Infrastructure Initialization +### Phase 1: Local Infrastructure Initialization (Kind) -1. **Container Registry (ECR)**: +> ### Environment Requirements +> | # | Component | Purpose | How to create it | +> | :---: | :--- | :--- | :--- | +> | 1 | **EC2 (Ubuntu)**| Host Instance | Launch `t3.medium` (Ubuntu 24.04) | +> | 2 | **Docker** | Container Runtime | Install via `apt` | +> | 3 | **Kind** | Local Kubernetes Cluster | Install via binary | +> | 4 | **Gemini API Key** | Enables AI assistant responses | Store in Kubernetes Secret | +> | 5 | **kubectl/Helm** | Cluster management | Install binaries | - - Establish a private ECR repository named `devsecops-bankapp`. +#### Step 0 — EC2 Launch & Docker Setup - ![ECR](screenshots/2.png) +1. **Launch Instance**: + - AMI: **Ubuntu Server 24.04 LTS**. + - Type: **t3.medium** (Min 2 vCPU, 4GB RAM). + - Security Group: Allow **22 (SSH)**, **80 (HTTP)**, **443 (HTTPS)**, and **8081 (ArgoCD)**. -2. **Application Server (EC2)**: +2. **Update & Install Docker & Helm**: + ```bash + sudo apt update && sudo apt upgrade -y + + # Install Docker + sudo apt install docker.io -y + sudo usermod -aG docker $USER && newgrp docker - - Deploy an Ubuntu 22.04 instance with below `User Data`. + # Install Helm + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + ``` +#### Step 1 — Infrastructure Initialization (Kind) + - Install Kind: + ```bash - #!/bin/bash + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64 - sudo apt update - sudo apt install -y docker.io docker-compose-v2 jq - sudo usermod -aG docker ubuntu - sudo newgrp docker - sudo snap install aws-cli --classic - ``` + chmod +x ./kind - - Configure Security Group to open inbound rule for Port 22 (Management) and Port 8080 (Service). + sudo mv ./kind /usr/local/bin/kind + ``` - > Better to give `name` to Security Group created. + - Install Kubectl - - Create an IAM Instance Profile(IAM EC2 role) containing permissions: - - `AmazonEC2ContainerRegistryPowerUser` - - `AWSSecretsManagerClientReadOnlyAccess` + ```bash + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" - ![Permissions](screenshots/3.png) + sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + + kubectl version --client + ``` - - Attach it to Application EC2. Select EC2 -> Actions -> Security -> Modify IAM role -> Attach created IAM role. + - Clone this repository and navigate to the project directory - ![IAM role](screenshots/4.png) + - Create Kind Cluster: + + ```bash + chmod +x scripts/kind-setup.sh + + ./scripts/kind-setup.sh + ``` + + - **Install Gateway API & Envoy Gateway**: (Crucial for Networking) + + ```bash + # 1. Install Gateway API CRDs + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml + + # 2. Install Envoy Gateway via Helm + helm install eg oci://docker.io/envoyproxy/gateway-helm \ + --version v1.7.1 \ + -n envoy-gateway-system \ + --create-namespace \ + --set service.type=NodePort + ``` + + - Wait for Envoy Gateway to be ready: - - Connect to EC2 Instance and Run below command to check whether IAM role is working or not. + ```bash + kubectl wait -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available --timeout=5m + ``` + + - No local AI runtime is required. This project uses Gemini over HTTPS. + + - **Verification**: Confirm Gateway API CRDs and Envoy Gateway are installed and healthy. ```bash - aws sts get-caller-identity + kubectl get crd gateways.gateway.networking.k8s.io + kubectl get pods -n envoy-gateway-system ``` - You will get your account details with IAM role assumed. +--- -3. **AI Engine Tier (Ollama)**: - - Deploy a dedicated Ubuntu EC2 instance. - - Open Inbound Port `11434` from the Application EC2 Security Group. +### Phase 2: Security and Pipeline Configuration - > Better to give `name` to Security Group created. - - ![ollama-sg](screenshots/8.png) +#### 1. Docker Hub Repository +- Create a repository named `devsecops-bankapp` on [hub.docker.com](https://hub.docker.com/). - - Automate initialization using the [ollama-setup.sh](scripts/ollama-setup.sh) script via EC2 User Data. - - ![user-data](screenshots/9.png) +#### 2. GitHub Repository Secrets +Configure the following Action Secrets in **Settings → Secrets and variables → Actions**: + +| Secret Name | Description | +| :--- | :--- | +| `DOCKERHUB_USERNAME` | Your Docker Hub username | +| `DOCKERHUB_TOKEN` | Your Docker Hub Personal Access Token (PAT) | +| `DOCKERHUB_REPO` | Your full repo name (e.g., `username/devsecops-bankapp`) | +| `NVD_API_KEY` | Free API key from [nvd.nist.gov](https://nvd.nist.gov/developers/request-an-api-key) | + +> **Note**: `GITHUB_TOKEN` is used automatically by `cd.yml` to commit the updated `values.yaml` — ensure **Settings → Actions → General → Workflow permissions** is set to **"Read and write permissions"**. + +#### Obtaining the NVD API Key (Optional but Recommended) +The `NVD_API_KEY` raises the NVD API rate limit from ~5 requests/30s to 50 requests/30s, reducing the OWASP Dependency Check scan time from 30+ minutes to under 8 minutes. Without it the SCA job will time out. - - Verify the AI engine is responsive and the model is pulled in `AI engine EC2`: +**Step 1: Request the API Key** +- Go to [https://nvd.nist.gov/developers/request-an-api-key](https://nvd.nist.gov/developers/request-an-api-key). +- Enter your `Organization name`, `email address`, and select `organization type`. +- Accept **Terms of Use** and Click **Submit**. + + ![request](screenshots/22.png) + +**Step 2: Activate the API Key** +- Check your email inbox for a message from `nvd-noreply@nist.gov`. - ```bash - ollama list - ``` + ![email](screenshots/25.png) - ![ollama-list](screenshots/21.png) +- Click the **activation link** in the email. +- Enter `UUID` provided in email and Enter `Email` to activate +- The link confirms your key and marks it as active. + + ![api-activate](screenshots/23.png) + +**Step 3: Get the API Key** +- After clicking the activation link, the page will generate your API key. +- Copy and save it securely. + + ![api-key](screenshots/24.png) + +**Step 4**: Add as GitHub Secret named `NVD_API_KEY`. + + ![github-secret](screenshots/15.png) --- -### Phase 2: Security and Identity Configuration +### Phase 3: Cloud-Native Deployment (GitOps + Gateway API) -The deployment pipeline utilizes OpenID Connect (OIDC) for secure, keyless authentication between GitHub and AWS. +#### Step 1 — Create Namespace, DB Secret, and Gemini Secret -1. **IAM Identity Provider**: - - Provider URL: `https://token.actions.githubusercontent.com` - - Audience: `sts.amazonaws.com` +```bash +kubectl create namespace bankapp-prod - ![identity-provider](screenshots/10.png) +kubectl create secret generic bankapp-db-secrets \ + --from-literal=password= \ + -n bankapp-prod -2. **Deployment Role**: - - Click on created `Identity provider` - - Asign & Create a role named `GitHubActionsRole`. - - Enter following details: - - `Identity provider`: Select created one. - - `Audience`: Select created one. - - `GitHub organization`: Your GitHub Username or Orgs Name where this repo is located. - - `GitHub repository`: Write the Repository name of this project. `(e.g, DevSecOps-Bankapp)` - - `GitHub branch`: branch to use for this project `(e.g, devsecops)` - - Click on `Next` +kubectl create secret generic bankapp-ai-secrets \ + --from-literal=gemini-api-key= \ + -n bankapp-prod +``` - ![role](screenshots/11.png) +> Get Gemini API key from [Google AI Studio](https://aistudio.google.com/api-keys). This key is required for the AI assistant functionality in the BankApp backend. If you do not have a Gemini API key, you can still deploy and run the application, but AI-powered features will not work. - - Assign `AmazonEC2ContainerRegistryPowerUser` permissions. +#### Step 2 — Verify Kind Networking - ![iam permission](screenshots/12.png) +Ensure your EC2 Security Group allows traffic on ports **80** and **443** (host ports mapped by Kind). - - Click on `Next`, Enter name of role and click on `Create role`. +> **Note**: In a Kind cluster, Envoy is exposed through NodePorts **30080/30443**, and `kind-setup.sh` maps host ports **80/443** to those NodePorts. - ![iam role](screenshots/13.png) +> **Networking Update**: This project uses **Gateway API + Envoy Gateway** (not Kubernetes Ingress). Ingress is deprecated, and all traffic exposure is handled declaratively via `Gateway`, `HTTPRoute`, and `EnvoyProxy` manifests in the Helm chart. ---- +#### Step 2.1 — Configure nip.io Hostname + TLS Values -### Phase 3: Secrets and Pipeline Configuration +Update `charts/bankapp/values.yaml` before ArgoCD sync: -#### 1. AWS Secrets Manager -Create a secret named `bankapp/prod-secrets` in `Other type of secret` with the following key-value pairs: +> **Important**: `gateway.host` must be a real `.nip.io` value and `email` must be valid for Let's Encrypt ACME registration. +> +> **Important**: The deployment reads `GEMINI_API_KEY` from secret `bankapp-ai-secrets` and key `gemini-api-key`. +> If you change these names, update `charts/bankapp/templates/deployment.yaml` accordingly. -| Secret Key | Description | Sample/Default Value | -| :--- | :--- | :--- | -| `DB_HOST` | The MySQL container service name | `db` | -| `DB_PORT` | The database port | `3306` | -| `DB_NAME` | The application database name | `bankappdb` | -| `DB_USER` | The database username | `bankuser` | -| `DB_PASSWORD` | The database password | `Test@123` | -| `OLLAMA_URL` | The private URL for the AI tier | `http://:11434` | +> **Important**: Replace any placeholder/default values in `charts/bankapp/values.yaml` (especially `gateway.host` and `gateway.tls.certManager.email`) with your own environment values before syncing with ArgoCD. -![aws-ssm](screenshots/14.png) +#### Step 2.2 — Install Cert-Manager (Required for HTTPS) -#### 2. GitHub Repository Secrets -Configure the following Action Secrets within your GitHub repository settings: +```bash +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml -| Secret Name | Description | -| :--- | :--- | -| `AWS_ROLE_ARN` | The ARN of the `GitHubActionsRole` | -| `AWS_REGION` | The AWS region where resources are deployed | -| `AWS_ACCOUNT_ID` | Your 12-digit AWS account number | -| `ECR_REPOSITORY` | The name of the ECR repository (`devsecops-bankapp`) | -| `EC2_HOST` | The public IP address of the Application EC2 | -| `EC2_USER` | The SSH username (default is `ubuntu`) | -| `EC2_SSH_KEY` | The content of your private SSH key (`.pem` file) | -| `NVD_API_KEY` | Free API key from [nvd.nist.gov](https://nvd.nist.gov/developers/request-an-api-key) for OWASP SCA scans | +kubectl wait -n cert-manager --for=condition=Available deployment/cert-manager --timeout=5m -> **Note**: The `NVD_API_KEY` raises the NVD API rate limit from ~5 requests/30s to 50 requests/30s, reducing the OWASP Dependency Check scan time from 30+ minutes to under 8 minutes. Without it the SCA job will time out. +kubectl wait -n cert-manager --for=condition=Available deployment/cert-manager-cainjector --timeout=5m -#### Obtaining the NVD API Key +kubectl wait -n cert-manager --for=condition=Available deployment/cert-manager-webhook --timeout=5m -**Step 1: Request the API Key** -- Go to [https://nvd.nist.gov/developers/request-an-api-key](https://nvd.nist.gov/developers/request-an-api-key). -- Enter your `Organzation name`, `email address`, and select `organization type`. -- Accept **Terms of Use** and Click **Submit**. +# Required when using HTTP01 solver with Gateway API +kubectl -n cert-manager patch deploy cert-manager --type='json' \ + -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--enable-gateway-api"}]' - ![request](screenshots/22.png) +kubectl -n cert-manager rollout restart deploy cert-manager -**Step 2: Activate the API Key** -- Check your email inbox for a message from `nvd-noreply@nist.gov`. +kubectl -n cert-manager rollout status deploy cert-manager - ![email](screenshots/25.png) +# Verify +kubectl get pods -n cert-manager -- Click the **activation link** in the email. -- Enter `UUID` provided in email and Enter `Email` to activate -- The link confirms your key and marks it as active. +``` - ![api-activate](screenshots/23.png) +#### Step 3 — Install ArgoCD -**Step 3: Get the API Key** -- After clicking the activation link, the page will generate your API key. -- Copy and save it securely. +Install ArgoCD manually after Kind + Envoy setup: - ![api-key](screenshots/24.png) +```bash +kubectl create namespace argocd -**Step 4: Add as GitHub Secret** -- Go to your repository on GitHub. -- Navigate to **Settings** → **Secrets and variables** → **Actions** → **New repository secret**. -- Name: `NVD_API_KEY` -- Value: Paste the copied API key. -- Click **Add Secret**. +kubectl apply -n argocd \ + -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml -![github-secret](screenshots/15.png) +kubectl wait -n argocd --for=condition=Ready pods --all --timeout=5m +``` ---- +#### Step 4 — Login to ArgoCD -## Continuous Integration and Deployment +Fetch initial admin password and login to ArgoCD UI: -The DevSecOps lifecycle is orchestrated through the [DevSecOps Main Pipeline](.github/workflows/devsecops-main.yml), which securely sequences three modular workflows: [CI](.github/workflows/ci.yml), [Build](.github/workflows/build.yml), and [CD](.github/workflows/cd.yml). Together they enforce **9 sequential security gates** before any code reaches production. Every `git push` to the `main` or `devsecops` branch triggers the full pipeline automatically. +```bash +kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d && echo +``` -| Gate | Job | Tool | Action | -| :---: | :--- | :--- | :--- | -| 1 | `gitleaks` | Gitleaks | **Strict**: Fails if any secrets are found in history. | -| 2 | `lint` | Checkstyle | **Audit**: Reports style violations but doesn't block (Google Style). | -| 3 | `sast` | Semgrep | **Strict**: Scans code for vulnerabilities. Fails on findings. | -| 4 | `sca` | OWASP Dependency Check | **Strict**: Fails if any dependency has CVSS > 7.0. | -| 5 | `build` | Maven | Standard build and test stage. | -| 6 | `image_scan` | Trivy | **Strict**: Scans Docker image layers. Fails on any High/Critical CVE. | -| 7 | `push_to_ecr` | Amazon ECR | Pushes the verified image to AWS ECR using OIDC. | -| 8 | `deploy` | SSH / Docker Compose | Fetches secrets from AWS Secrets Manager and recreates the container. | -| 9 | `dast` | OWASP ZAP | **Audit Mode**: Comprehensive scan that reports findings as artifacts, but does not block the pipeline. | +Expose ArgoCD UI (if not externally exposed): -All scan reports (OWASP, Trivy, ZAP) are uploaded as downloadable **Artifacts** in each GitHub Actions run, YOu can look into the **Artifacts**. +```bash +kubectl port-forward svc/argocd-server -n argocd 8081:80 --address 0.0.0.0 & +``` -- CI/CD +Login via `http://:8081` with user `admin`. - ![github-actions](screenshots/16.png) +#### Step 5 — Deploy via ArgoCD (Apply Manifest) -- Artifacts +```bash +kubectl apply -f gitops/argocd-app.yaml +``` + +**ArgoCD Application** (`gitops/argocd-app.yaml`): +- Points to `charts/bankapp` in this repo. +- Deploys to namespace: `bankapp-prod`. +- Auto-sync enabled with `prune: true` and `selfHeal: true`. + + +ArgoCD will sync `charts/bankapp` and deploy all resources. + +![argocd-app-healthy](screenshots/argocd-app-healthy.png) + +#### Step 6 — Verify Access (Gateway API & nip.io) + +Once the application is synced by ArgoCD, the Gateway resource will automatically trigger the creation of the Envoy data-plane service. Thanks to our declarative configuration, this service is created as a **NodePort** with the correct mappings (30080/30443) automatically. + +Access your app at: +- **HTTP**: `http://.nip.io` +- **HTTPS**: `https://.nip.io` + +**Verification commands**: +```bash +kubectl get gateway bankapp-gateway -n bankapp-prod +kubectl get svc -n envoy-gateway-system +kubectl get certificate -n bankapp-prod +kubectl describe certificate bankapp-tls -n bankapp-prod +kubectl get certificaterequest,order,challenge -n bankapp-prod +``` + +If certificate issuance is stuck, inspect challenge reason: + +```bash +kubectl describe challenge -n bankapp-prod +``` + +If the reason shows `gateway api is not enabled`, re-run the cert-manager patch command from Step 2.2 and recreate ACME resources. + +> **Note**: `Challenge` resources are temporary. If certificate issuance already succeeded, `kubectl describe challenge -n bankapp-prod` may return `No resources found`, which is normal. +> Check `kubectl get certificate -n bankapp-prod` and confirm `READY=True`. + +> **Note**: For Let's Encrypt to verify your domain and enable HTTPS (optional Phase 3 check), ensure your EC2 Security Group allows traffic on ports **80** and **443**. + +#### Step 7 — Trigger the GitOps Pipeline + +Push code to `main`. GitHub Actions will: +1. Run 8 security gates. +2. Gate 8 commits the new tag to `values.yaml`. +3. ArgoCD auto-syncs the new image to the Kind cluster. + +![github-action-success](screenshots/github-action-success.png) + +![app-dashboard](screenshots/app-dashboard.png) + +![app-transaction](screenshots/app-transaction.png) + +#### AI Assistant Behavior (Current) + +- For account-specific intents like balance and transaction history, the backend can return deterministic answers directly from the database for reliability and speed. +- For open-ended prompts (for example: financial concepts), responses are generated through Gemini. +- Fast responses are therefore expected for balance/transaction questions and do not always indicate an external AI call. + +
+ +![ai-working](screenshots/ai-working-1.png) + +![ai-transaction](screenshots/ai-transaction-working.png) + +![ai-working](screenshots/ai-working-2.png) + +
- ![artifacts](screenshots/26.png) - --- -## Operational Verification +## Verification & Access -- **Process Status**: `docker ps` +| Check | Command | +| :--- | :--- | +| Cluster Nodes | `kubectl get nodes` | +| Application Pods | `kubectl get pods -n bankapp-prod` | +| ArgoCD Apps | `kubectl get applications -n argocd` | +| Gateway Status | `kubectl get gateway -n bankapp-prod` | +| HTTP Routes | `kubectl get httproute -n bankapp-prod` | + +**Access Points:** +* **BankApp (HTTP)**: `http://.nip.io/` +* **BankApp (HTTPS)**: `https://.nip.io/` +* **Nginx Demo**: `http://.nip.io/nginx` +* **ArgoCD UI**: `http://:8081` (via `kubectl port-forward`) + +**Path Routing:** +| Path | Backend | Description | +| :--- | :--- | :--- | +| `/` | BankApp (port 8080) | Spring Boot Banking Application | +| `/nginx` | Nginx (port 80) | Demo service to showcase Gateway API routing | - ![docker ps](screenshots/19.png) +--- -- **Application Working**: +## Helm Chart Structure - ![app](screenshots/20.png) +``` +charts/bankapp/ +├── Chart.yaml # Chart metadata (name: bankapp, version: 0.1.0) +├── values.yaml # All configurable values (image, DB, Nginx) +└── templates/ + ├── _helpers.tpl # Shared template helpers + ├── certificate.yaml # cert-manager Certificate for nip.io TLS + ├── deployment.yaml # BankApp Deployment (Docker Hub image, health probes) + ├── envoyproxy.yaml # Declarative NodePort configuration for Kind + ├── gateway.yaml # Gateway API — Gateway resource (ports 80/443) + ├── gatewayclass.yaml # Envoy Gateway Class (linked to EnvoyProxy) + ├── httproute.yaml # Gateway API — HTTPRoute (path-based routing) + ├── issuer.yaml # cert-manager ACME Issuer (Let's Encrypt) + ├── mysql.yaml # MySQL 8.0 Deployment + ClusterIP Service + ├── nginx.yaml # Nginx Demo Deployment + Service + └── service.yaml # BankApp ClusterIP Service (port 8080) +``` + +--- -- **Database Connectivity**: +## DB Verification - ```bash - docker exec -it db mysql -u -p bankappdb -e "SELECT * FROM accounts;" - ``` +```bash +kubectl exec -it -n bankapp-prod -- mysql -u bankuser -p bankapp -e "SELECT * FROM accounts;" +``` - ![mysql-result](screenshots/17.png) +![mysql-test](screenshots/27.png) - > **ZAP** is automatically created by **DAST - OWASP ZAP Baseline Scan** job in [cd.yml](.github/workflows/cd.yml). Read more about it(How, Why it does) on google... +--- -- **Network Validation**: +## 🧹 Cleanup - ```bash - nc -zv 11434 - ``` +To delete the local infrastructure: - ![ollama-success](screenshots/18.png) +1. **Delete Kind Cluster**: + ```bash + kind delete cluster --name bankapp-kind-cluster + ``` +2. **Delete EC2 Instance** + ---
Happy Learning -**TrainWithShubham** +**TrainWithShubham**
\ No newline at end of file diff --git a/app-tier.yml b/app-tier.yml index 21a6bf9..0fbdfc0 100644 --- a/app-tier.yml +++ b/app-tier.yml @@ -21,7 +21,7 @@ services: retries: 5 bankapp: - image: ${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG} + image: ${DOCKERHUB_REPO}:${IMAGE_TAG} container_name: bankapp ports: - "8080:8080" @@ -31,7 +31,10 @@ services: MYSQL_DATABASE: ${DB_NAME} MYSQL_USER: ${DB_USER} MYSQL_PASSWORD: ${DB_PASSWORD} - OLLAMA_URL: ${OLLAMA_URL} + GEMINI_API_KEY: ${GEMINI_API_KEY} + GEMINI_MODEL: ${GEMINI_MODEL:-gemini-3-flash-preview} + GEMINI_API_URL: ${GEMINI_API_URL:-https://generativelanguage.googleapis.com/v1beta/models} + GEMINI_MAX_OUTPUT_TOKENS: ${GEMINI_MAX_OUTPUT_TOKENS:-512} depends_on: db: condition: service_healthy diff --git a/charts/bankapp/Chart.yaml b/charts/bankapp/Chart.yaml new file mode 100644 index 0000000..46f032b --- /dev/null +++ b/charts/bankapp/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: bankapp +description: A Helm chart for DevSecOps Bankapp on EKS with Gateway API +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/charts/bankapp/templates/_helpers.tpl b/charts/bankapp/templates/_helpers.tpl new file mode 100644 index 0000000..3ac0501 --- /dev/null +++ b/charts/bankapp/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bankapp.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bankapp.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bankapp.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bankapp.labels" -}} +helm.sh/chart: {{ include "bankapp.chart" . }} +{{ include "bankapp.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bankapp.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bankapp.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/bankapp/templates/certificate.yaml b/charts/bankapp/templates/certificate.yaml new file mode 100644 index 0000000..f26d0bd --- /dev/null +++ b/charts/bankapp/templates/certificate.yaml @@ -0,0 +1,12 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "bankapp.fullname" . }}-tls +spec: + secretName: {{ .Values.gateway.tls.secretName }} + commonName: {{ .Values.gateway.host | quote }} + dnsNames: + - {{ .Values.gateway.host | quote }} + issuerRef: + name: {{ .Values.gateway.tls.certManager.issuerName }} + kind: Issuer diff --git a/charts/bankapp/templates/deployment.yaml b/charts/bankapp/templates/deployment.yaml new file mode 100644 index 0000000..9ad4208 --- /dev/null +++ b/charts/bankapp/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "bankapp.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "bankapp.selectorLabels" . | nindent 8 }} + spec: + initContainers: + - name: wait-for-db + image: busybox:1.28 + command: ['sh', '-c', "until nc -zv {{ .Values.database.host }} 3306; do echo waiting for mysql; sleep 2; done;"] + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: MYSQL_HOST + value: {{ .Values.database.host | quote }} + - name: MYSQL_PORT + value: {{ .Values.database.port | quote }} + - name: MYSQL_DATABASE + value: {{ .Values.database.name | quote }} + - name: MYSQL_USER + value: {{ .Values.database.user | quote }} + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: bankapp-db-secrets + key: password + - name: AI_TIMEOUT_CONNECT_MS + value: {{ .Values.ai.timeoutConnectMs | quote }} + - name: AI_TIMEOUT_READ_MS + value: {{ .Values.ai.timeoutReadMs | quote }} + - name: GEMINI_MODEL + value: {{ .Values.gemini.model | quote }} + - name: GEMINI_MAX_OUTPUT_TOKENS + value: {{ .Values.gemini.maxOutputTokens | quote }} + - name: GEMINI_API_URL + value: {{ .Values.gemini.apiUrl | quote }} + - name: GEMINI_API_KEY + valueFrom: + secretKeyRef: + name: bankapp-ai-secrets + key: gemini-api-key + optional: true + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: http + initialDelaySeconds: 30 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: http + initialDelaySeconds: 30 + resources: + {{- toYaml .Values.resources | nindent 12 }} diff --git a/charts/bankapp/templates/envoyproxy.yaml b/charts/bankapp/templates/envoyproxy.yaml new file mode 100644 index 0000000..b42ec17 --- /dev/null +++ b/charts/bankapp/templates/envoyproxy.yaml @@ -0,0 +1,24 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: {{ .Values.gateway.envoyProxy.name }} + namespace: {{ .Release.Namespace }} +spec: + provider: + type: Kubernetes + kubernetes: + envoyService: + type: {{ .Values.gateway.envoyProxy.serviceType }} + patch: + type: StrategicMerge + value: + spec: + ports: + - name: http + port: 80 + targetPort: 10080 + nodePort: {{ .Values.gateway.envoyProxy.nodePorts.http }} + - name: https + port: 443 + targetPort: 10443 + nodePort: {{ .Values.gateway.envoyProxy.nodePorts.https }} \ No newline at end of file diff --git a/charts/bankapp/templates/gateway.yaml b/charts/bankapp/templates/gateway.yaml new file mode 100644 index 0000000..6007b31 --- /dev/null +++ b/charts/bankapp/templates/gateway.yaml @@ -0,0 +1,28 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: {{ .Values.gateway.name }} + annotations: + argocd.argoproj.io/ignore-healthcheck: "true" +spec: + gatewayClassName: {{ .Values.gateway.className }} + listeners: + - name: http + protocol: HTTP + port: 80 + hostname: {{ .Values.gateway.host | quote }} + allowedRoutes: + namespaces: + from: Same + - name: https + protocol: HTTPS + port: 443 + hostname: {{ .Values.gateway.host | quote }} + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: {{ .Values.gateway.tls.secretName }} + allowedRoutes: + namespaces: + from: Same diff --git a/charts/bankapp/templates/gatewayclass.yaml b/charts/bankapp/templates/gatewayclass.yaml new file mode 100644 index 0000000..1dc254f --- /dev/null +++ b/charts/bankapp/templates/gatewayclass.yaml @@ -0,0 +1,11 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: {{ .Values.gateway.className }} +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: {{ .Values.gateway.envoyProxy.name }} + namespace: {{ .Release.Namespace }} diff --git a/charts/bankapp/templates/httproute.yaml b/charts/bankapp/templates/httproute.yaml new file mode 100644 index 0000000..93f5873 --- /dev/null +++ b/charts/bankapp/templates/httproute.yaml @@ -0,0 +1,33 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "bankapp.fullname" . }}-http-route +spec: + hostnames: + - {{ .Values.gateway.host | quote }} + parentRefs: + - name: {{ .Values.gateway.name }} + sectionName: http + - name: {{ .Values.gateway.name }} + sectionName: https + rules: + - matches: + - path: + type: PathPrefix + value: /nginx + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + backendRefs: + - name: {{ include "bankapp.fullname" . }}-nginx + port: {{ .Values.nginx.port }} + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ include "bankapp.fullname" . }} + port: {{ .Values.service.port }} diff --git a/charts/bankapp/templates/issuer.yaml b/charts/bankapp/templates/issuer.yaml new file mode 100644 index 0000000..da02a03 --- /dev/null +++ b/charts/bankapp/templates/issuer.yaml @@ -0,0 +1,16 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ .Values.gateway.tls.certManager.issuerName }} +spec: + acme: + email: {{ .Values.gateway.tls.certManager.email | quote }} + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: {{ .Values.gateway.tls.certManager.issuerName }}-account-key + solvers: + - http01: + gatewayHTTPRoute: + parentRefs: + - name: {{ .Values.gateway.name }} + kind: Gateway diff --git a/charts/bankapp/templates/mysql.yaml b/charts/bankapp/templates/mysql.yaml new file mode 100644 index 0000000..1d07551 --- /dev/null +++ b/charts/bankapp/templates/mysql.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: standard # Generic default; Kind uses 'standard' + resources: + requests: + storage: 10Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.database.host }} + labels: + app: mysql +spec: + replicas: 1 + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: bankapp-db-secrets + key: password + - name: MYSQL_DATABASE + value: {{ .Values.database.name | quote }} + - name: MYSQL_USER + value: {{ .Values.database.user | quote }} + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: bankapp-db-secrets + key: password + securityContext: + allowPrivilegeEscalation: false + ports: + - containerPort: 3306 + name: mysql + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + livenessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: mysql-data + persistentVolumeClaim: + claimName: mysql-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.database.host }} +spec: + ports: + - port: 3306 + selector: + app: mysql diff --git a/charts/bankapp/templates/nginx.yaml b/charts/bankapp/templates/nginx.yaml new file mode 100644 index 0000000..bfb35a1 --- /dev/null +++ b/charts/bankapp/templates/nginx.yaml @@ -0,0 +1,36 @@ +{{- if .Values.nginx.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }}-nginx + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: {{ .Values.nginx.image }} + securityContext: + allowPrivilegeEscalation: false + ports: + - containerPort: {{ .Values.nginx.port }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }}-nginx +spec: + ports: + - port: {{ .Values.nginx.port }} + targetPort: {{ .Values.nginx.port }} + selector: + app: nginx +{{- end -}} diff --git a/charts/bankapp/templates/service.yaml b/charts/bankapp/templates/service.yaml new file mode 100644 index 0000000..496fb40 --- /dev/null +++ b/charts/bankapp/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: 8080 + protocol: TCP + name: http + selector: + {{- include "bankapp.selectorLabels" . | nindent 4 }} diff --git a/charts/bankapp/values.yaml b/charts/bankapp/values.yaml new file mode 100644 index 0000000..5c1eae5 --- /dev/null +++ b/charts/bankapp/values.yaml @@ -0,0 +1,60 @@ +# Default values for bankapp. +# This is a YAML-formatted file. + +replicaCount: 1 + +image: + repository: "amitabhdevops/devsecops-bankapp" # Your Docker Hub repo URI + pullPolicy: IfNotPresent + tag: "a4feabc43d587ed593b71c3f6c88539d73c5274d" + +service: + type: ClusterIP + port: 8080 + +database: + host: "db-service" + port: 3306 + name: "bankapp" + user: "bankuser" + password: "" # To be provided from Secrets + +ai: + timeoutConnectMs: 3000 + timeoutReadMs: 30000 + +gemini: + model: "gemini-3-flash-preview" + maxOutputTokens: 512 + apiUrl: "https://generativelanguage.googleapis.com/v1beta/models" + +gateway: + name: "bankapp-gateway" + className: "envoy" # Envoy Gateway Controller + host: "54.91.43.65.nip.io" # Update with your nip.io host (e.g., .nip.io) + envoyProxy: + name: "bankapp-proxy-config" + serviceType: NodePort + nodePorts: + http: 30080 + https: 30443 + tls: + enabled: true + secretName: "bankapp-tls" + certManager: + enabled: true + issuerName: "letsencrypt-prod" + email: "amitabhdevops2004@gmail.com" # Update with your email for cert-manager notifications + +nginx: + enabled: true + image: nginx:latest + port: 80 + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi diff --git a/docker-compose.yml b/docker-compose.yml index 2220669..d6130e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,8 @@ +# ============================================================== +# LOCAL DEVELOPMENT ONLY — DO NOT USE IN PRODUCTION +# All credentials here are hardcoded for local dev convenience. +# Production deployment uses Kind (Local/VM) + Helm + Kubernetes Secrets. +# ============================================================== services: mysql: image: mysql:8.0 @@ -18,36 +23,6 @@ services: networks: - bankapp-net - ollama: - image: ollama/ollama - container_name: bankapp-ollama - ports: - - "11434:11434" - volumes: - - ollama-data:/root/.ollama - healthcheck: - test: ["CMD", "ollama", "list"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - bankapp-net - - ollama-pull-model: - image: ollama/ollama - container_name: ollama-pull-model - environment: - - OLLAMA_HOST=ollama - volumes: - - ollama-data:/root/.ollama - depends_on: - ollama: - condition: service_healthy - entrypoint: /bin/sh - command: -c "ollama pull tinyllama" - networks: - - bankapp-net - bankapp: build: . container_name: bankapp @@ -59,18 +34,17 @@ services: MYSQL_DATABASE: bankappdb MYSQL_USER: root MYSQL_PASSWORD: Test@123 - OLLAMA_URL: http://ollama:11434 + GEMINI_API_KEY: ${GEMINI_API_KEY} + GEMINI_MODEL: ${GEMINI_MODEL:-gemini-3-flash-preview} + GEMINI_MAX_OUTPUT_TOKENS: ${GEMINI_MAX_OUTPUT_TOKENS:-512} depends_on: mysql: condition: service_healthy - ollama-pull-model: - condition: service_completed_successfully networks: - bankapp-net volumes: mysql-data: - ollama-data: networks: bankapp-net: diff --git a/gitops/argocd-app.yaml b/gitops/argocd-app.yaml new file mode 100644 index 0000000..76ed5c4 --- /dev/null +++ b/gitops/argocd-app.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: bankapp + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/Amitabh-DevOps/DevSecOps-Bankapp.git + targetRevision: main + path: charts/bankapp + helm: + valueFiles: + - values.yaml + destination: + server: https://kubernetes.default.svc + namespace: bankapp-prod + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/screenshots/1.png b/screenshots/1.png index b0c823a..9ca3275 100644 Binary files a/screenshots/1.png and b/screenshots/1.png differ diff --git a/screenshots/10.png b/screenshots/10.png deleted file mode 100644 index 46b032d..0000000 Binary files a/screenshots/10.png and /dev/null differ diff --git a/screenshots/11.png b/screenshots/11.png deleted file mode 100644 index 8640dfc..0000000 Binary files a/screenshots/11.png and /dev/null differ diff --git a/screenshots/12.png b/screenshots/12.png deleted file mode 100644 index 3cfe83a..0000000 Binary files a/screenshots/12.png and /dev/null differ diff --git a/screenshots/13.png b/screenshots/13.png deleted file mode 100644 index 9ea11a8..0000000 Binary files a/screenshots/13.png and /dev/null differ diff --git a/screenshots/14.png b/screenshots/14.png deleted file mode 100644 index 3113269..0000000 Binary files a/screenshots/14.png and /dev/null differ diff --git a/screenshots/15.png b/screenshots/15.png index cb8113f..6b53107 100644 Binary files a/screenshots/15.png and b/screenshots/15.png differ diff --git a/screenshots/16.png b/screenshots/16.png deleted file mode 100644 index 33b0436..0000000 Binary files a/screenshots/16.png and /dev/null differ diff --git a/screenshots/17.png b/screenshots/17.png deleted file mode 100644 index 2159b9b..0000000 Binary files a/screenshots/17.png and /dev/null differ diff --git a/screenshots/18.png b/screenshots/18.png deleted file mode 100644 index c42caf8..0000000 Binary files a/screenshots/18.png and /dev/null differ diff --git a/screenshots/19.png b/screenshots/19.png deleted file mode 100644 index 47cce8e..0000000 Binary files a/screenshots/19.png and /dev/null differ diff --git a/screenshots/2.png b/screenshots/2.png deleted file mode 100644 index 236829b..0000000 Binary files a/screenshots/2.png and /dev/null differ diff --git a/screenshots/20.png b/screenshots/20.png deleted file mode 100644 index 9a472bd..0000000 Binary files a/screenshots/20.png and /dev/null differ diff --git a/screenshots/21.png b/screenshots/21.png deleted file mode 100644 index 180e0d5..0000000 Binary files a/screenshots/21.png and /dev/null differ diff --git a/screenshots/26.png b/screenshots/26.png deleted file mode 100644 index f8f473e..0000000 Binary files a/screenshots/26.png and /dev/null differ diff --git a/screenshots/27.png b/screenshots/27.png new file mode 100644 index 0000000..084b9ef Binary files /dev/null and b/screenshots/27.png differ diff --git a/screenshots/3.png b/screenshots/3.png deleted file mode 100644 index c5f9ab1..0000000 Binary files a/screenshots/3.png and /dev/null differ diff --git a/screenshots/4.png b/screenshots/4.png deleted file mode 100644 index 95530eb..0000000 Binary files a/screenshots/4.png and /dev/null differ diff --git a/screenshots/5.png b/screenshots/5.png deleted file mode 100644 index 5135050..0000000 Binary files a/screenshots/5.png and /dev/null differ diff --git a/screenshots/6.png b/screenshots/6.png deleted file mode 100644 index 2e966bc..0000000 Binary files a/screenshots/6.png and /dev/null differ diff --git a/screenshots/7.png b/screenshots/7.png deleted file mode 100644 index 8452559..0000000 Binary files a/screenshots/7.png and /dev/null differ diff --git a/screenshots/8.png b/screenshots/8.png deleted file mode 100644 index 647947c..0000000 Binary files a/screenshots/8.png and /dev/null differ diff --git a/screenshots/9.png b/screenshots/9.png deleted file mode 100644 index f61115b..0000000 Binary files a/screenshots/9.png and /dev/null differ diff --git a/screenshots/ai-transaction-working.png b/screenshots/ai-transaction-working.png new file mode 100644 index 0000000..9900c26 Binary files /dev/null and b/screenshots/ai-transaction-working.png differ diff --git a/screenshots/ai-working-1.png b/screenshots/ai-working-1.png new file mode 100644 index 0000000..b015f20 Binary files /dev/null and b/screenshots/ai-working-1.png differ diff --git a/screenshots/ai-working-2.png b/screenshots/ai-working-2.png new file mode 100644 index 0000000..81c4589 Binary files /dev/null and b/screenshots/ai-working-2.png differ diff --git a/screenshots/app-dashboard.png b/screenshots/app-dashboard.png new file mode 100644 index 0000000..7dd70e9 Binary files /dev/null and b/screenshots/app-dashboard.png differ diff --git a/screenshots/app-transaction.png b/screenshots/app-transaction.png new file mode 100644 index 0000000..de4ec5c Binary files /dev/null and b/screenshots/app-transaction.png differ diff --git a/screenshots/architecture.png b/screenshots/architecture.png new file mode 100644 index 0000000..a8dcc14 Binary files /dev/null and b/screenshots/architecture.png differ diff --git a/screenshots/argocd-app-healthy.png b/screenshots/argocd-app-healthy.png new file mode 100644 index 0000000..e4bd6e9 Binary files /dev/null and b/screenshots/argocd-app-healthy.png differ diff --git a/screenshots/github-action-success.png b/screenshots/github-action-success.png new file mode 100644 index 0000000..9543f5a Binary files /dev/null and b/screenshots/github-action-success.png differ diff --git a/scripts/kind-setup.sh b/scripts/kind-setup.sh new file mode 100644 index 0000000..57d5f52 --- /dev/null +++ b/scripts/kind-setup.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Kind Cluster Setup Script - Minimal +# focus strictly on Kind cluster creation and public IP detection + +CLUSTER_NAME="bankapp-kind-cluster" + +echo "Creating Kind Cluster: $CLUSTER_NAME..." +# Create kind cluster with 80 and 443 ports forwarded for the Gateway +cat < /etc/systemd/system/ollama.service.d/override.conf -[Service] -Environment="OLLAMA_HOST=0.0.0.0" -EOF - -# 4. Reload and Restart the service -systemctl daemon-reload -systemctl restart ollama - -# 6. Wait for Ollama server to be ready (Health Check) -echo "Waiting for Ollama server to start..." -MAX_RETRIES=30 -RETRY_COUNT=0 -while ! curl -s http://localhost:11434/api/tags > /dev/null; do - RETRY_COUNT=$((RETRY_COUNT+1)) - if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then - echo "Ollama server failed to start in time." - exit 1 - fi - sleep 2 -done - -# 7. Pull the required model -echo "Pulling tinyllama model..." -ollama pull tinyllama - -echo "Ollama setup complete and listening on port 11434" diff --git a/src/main/java/com/example/bankapp/service/ChatService.java b/src/main/java/com/example/bankapp/service/ChatService.java index 143b00a..a010b18 100644 --- a/src/main/java/com/example/bankapp/service/ChatService.java +++ b/src/main/java/com/example/bankapp/service/ChatService.java @@ -3,59 +3,173 @@ import com.example.bankapp.model.Account; import com.example.bankapp.model.Transaction; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.stereotype.Service; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; @Service public class ChatService { - @Value("${ollama.url}") - private String ollamaUrl; + @Value("${gemini.api.url:https://generativelanguage.googleapis.com/v1beta/models}") + private String geminiApiUrl; - @Value("${ollama.model}") - private String model; + @Value("${gemini.model:gemini-3-flash-preview}") + private String geminiModel; - private final RestTemplate restTemplate = new RestTemplate(); + @Value("${gemini.api.key:}") + private String geminiApiKey; + + @Value("${ai.timeout.connect-ms:3000}") + private int connectTimeoutMs; + + @Value("${ai.timeout.read-ms:30000}") + private int readTimeoutMs; + + @Value("${gemini.max-output-tokens:512}") + private int geminiMaxOutputTokens; + + private final RestTemplateBuilder restTemplateBuilder; private final AccountService accountService; - public ChatService(AccountService accountService) { + public ChatService(AccountService accountService, RestTemplateBuilder restTemplateBuilder) { this.accountService = accountService; + this.restTemplateBuilder = restTemplateBuilder; } public String chat(Account account, String userMessage) { List recent = accountService.getTransactionHistory(account); + String deterministicReply = tryBuildDeterministicReply(account, recent, userMessage); + if (deterministicReply != null) { + return deterministicReply; + } + String context = buildContext(account, recent); + RestTemplate restTemplate = restTemplateBuilder + .setConnectTimeout(Duration.ofMillis(connectTimeoutMs)) + .setReadTimeout(Duration.ofMillis(readTimeoutMs)) + .build(); + + try { + return askGemini(restTemplate, context, userMessage); + } catch (ResourceAccessException e) { + return "AI assistant is taking longer than expected. Please try again in a few seconds."; + } catch (Exception e) { + return "AI assistant is unavailable. Please try again shortly."; + } + } + + private String tryBuildDeterministicReply(Account account, List recent, String userMessage) { + String normalized = userMessage == null ? "" : userMessage.toLowerCase(Locale.ROOT); + + boolean asksBalance = normalized.contains("balance") || normalized.contains("current balance"); + if (asksBalance) { + return "Hi " + account.getUsername() + "! Your current balance is $" + formatMoney(account.getBalance()) + "."; + } + + boolean asksTransactions = normalized.contains("transaction") + || normalized.contains("transactions") + || normalized.contains("history") + || normalized.contains("statement"); + if (asksTransactions) { + if (recent.isEmpty()) { + return "Hi " + account.getUsername() + "! You do not have any transactions yet."; + } + + int limit = Math.min(recent.size(), 4); + StringBuilder response = new StringBuilder("Hi ") + .append(account.getUsername()) + .append("! Here are your recent transactions: "); + + for (int i = 0; i < limit; i++) { + Transaction t = recent.get(i); + if (i > 0) { + response.append("; "); + } + response.append(t.getTimestamp().toLocalDate()) + .append(" ") + .append(t.getType()) + .append(" $") + .append(formatMoney(t.getAmount())); + } + + response.append("."); + return response.toString(); + } + + return null; + } + + private String formatMoney(BigDecimal amount) { + return String.format(Locale.US, "%,.2f", amount); + } + + private String askGemini(RestTemplate restTemplate, String context, String userMessage) { + if (geminiApiKey == null || geminiApiKey.isBlank()) { + throw new IllegalStateException("GEMINI_API_KEY is missing"); + } + Map request = Map.of( - "model", model, - "messages", List.of( - Map.of("role", "system", "content", context), - Map.of("role", "user", "content", userMessage) + "system_instruction", Map.of( + "parts", List.of(Map.of("text", context)) + ), + "contents", List.of( + Map.of( + "role", "user", + "parts", List.of(Map.of("text", userMessage)) + ) ), - "stream", false + "generationConfig", Map.of( + "temperature", 0.2, + "maxOutputTokens", geminiMaxOutputTokens + ) ); - try { - Map response = restTemplate.postForObject( - ollamaUrl + "/api/chat", request, Map.class - ); - if (response != null && response.containsKey("message")) { - Map message = (Map) response.get("message"); - return message.get("content"); - } + String endpoint = geminiApiUrl + "/" + geminiModel + ":generateContent?key=" + geminiApiKey; + Map response = restTemplate.postForObject(endpoint, request, Map.class); + + if (response == null) { + return "Sorry, I couldn't process that."; + } + + List> candidates = (List>) response.getOrDefault("candidates", Collections.emptyList()); + if (candidates.isEmpty()) { return "Sorry, I couldn't process that."; - } catch (Exception e) { - return "AI assistant is unavailable. Please make sure Ollama is running."; } + + Map content = (Map) candidates.get(0).getOrDefault("content", Collections.emptyMap()); + List> parts = (List>) content.getOrDefault("parts", Collections.emptyList()); + if (parts.isEmpty()) { + return "Sorry, I couldn't process that."; + } + + StringBuilder merged = new StringBuilder(); + for (Map part : parts) { + Object text = part.get("text"); + if (text != null) { + if (merged.length() > 0) { + merged.append("\n"); + } + merged.append(text); + } + } + + return merged.length() == 0 ? "Sorry, I couldn't process that." : merged.toString(); } private String buildContext(Account account, List transactions) { StringBuilder sb = new StringBuilder(); sb.append("You are a helpful banking assistant for BankApp. "); - sb.append("Keep answers short and friendly (2-3 sentences max). "); + sb.append("Keep answers friendly and concise. "); + sb.append("If the user asks for transactions, list them clearly with key details. "); sb.append("\n\nCustomer details:"); sb.append("\n- Username: ").append(account.getUsername()); sb.append("\n- Balance: $").append(account.getBalance()); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d16bfa0..73c9391 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,9 +18,14 @@ spring.threads.virtual.enabled=true management.endpoints.web.exposure.include=health management.endpoint.health.show-details=when-authorized -# Ollama AI -ollama.url=${OLLAMA_URL:http://localhost:11434} -ollama.model=tinyllama +# Gemini AI +ai.timeout.connect-ms=${AI_TIMEOUT_CONNECT_MS:3000} +ai.timeout.read-ms=${AI_TIMEOUT_READ_MS:30000} + +gemini.api.url=${GEMINI_API_URL:https://generativelanguage.googleapis.com/v1beta/models} +gemini.model=${GEMINI_MODEL:gemini-3-flash-preview} +gemini.api.key=${GEMINI_API_KEY:} +gemini.max-output-tokens=${GEMINI_MAX_OUTPUT_TOKENS:512} # Structured logging logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n diff --git a/trivy.yaml b/trivy.yaml index c7ca33f..8345d96 100644 --- a/trivy.yaml +++ b/trivy.yaml @@ -8,3 +8,4 @@ vulnerability: ignore-unfixed: true severity: - CRITICAL + - HIGH