diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..0ecb653f --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Copy this file to .env and set real values before running docker-compose. +# NEVER commit the .env file to version control. +MYSQL_ROOT_PASSWORD= +SPRING_DATASOURCE_PASSWORD= +DUSER= +IMAGE= diff --git a/.gitignore b/.gitignore index 549e00a2..def745be 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ build/ ### VS Code ### .vscode/ + +### Security: prevent accidental commit of secrets ### +.env +*.env.local diff --git a/Dockerfile b/Dockerfile index 079acabe..589eb41c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,38 @@ #---------------------------------- -# Stage 1 +# Stage 1: Build #---------------------------------- -# Import docker image with maven installed -FROM maven:3.8.3-openjdk-17 as builder +# Security: pin base image to SHA digest to prevent supply-chain attacks via tag mutation +FROM maven:3.8.3-openjdk-17@sha256:8a66581a077762c8752a9f64f73cdd8c59e9c4446eb810417119e0436b075931 AS builder -# Add maintainer, so that new user will understand who had written this Dockerfile -MAINTAINER Madhup Pandey - -# Add labels to the image to filter out if we have multiple application running LABEL app=bankapp -# Set working directory WORKDIR /src -# Copy source code from local to container COPY . /src -# Build application and skip test cases RUN mvn clean install -DskipTests=true #-------------------------------------- -# Stage 2 +# Stage 2: Runtime #-------------------------------------- -# Import small size java image -FROM openjdk:17-alpine as deployer +# Security: replaced deprecated openjdk:17-alpine with actively maintained eclipse-temurin; +# pinned to SHA digest for reproducible builds +FROM eclipse-temurin:17-jre-alpine@sha256:02320dd4ce20e243dfb915c686089cf9315c763084fafbb12d5c9993aee18b57 + +# Security: remove unnecessary OS packages and caches from final image +RUN apk --no-cache upgrade \ + && rm -rf /var/cache/apk/* -# Copy build from stage 1 (builder) -COPY --from=builder /src/target/*.jar /src/target/bankapp.jar +# Security: create a non-root user to run the application (CIS Docker Benchmark 4.1) +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +COPY --from=builder --chown=appuser:appgroup /src/target/*.jar /app/bankapp.jar -# Expose application port EXPOSE 8080 -# Start the application -ENTRYPOINT ["java", "-jar", "/src/target/bankapp.jar"] +# Security: run as non-root user to limit blast radius of container compromise +USER appuser + +ENTRYPOINT ["java", "-jar", "/app/bankapp.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index 34642a09..67044e74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,12 @@ version: "3.8" services: mysql: - image: mysql:latest + # Security: pin image to SHA digest for reproducible, tamper-resistant builds + image: mysql:8.0@sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b container_name: mysql environment: - - MYSQL_ROOT_PASSWORD=Test@123 + # Security: credentials sourced from .env file or environment variables instead of hardcoded values + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env} - MYSQL_DATABASE=BankDB volumes: - bankapp-volume:/var/lib/mysql @@ -23,7 +25,8 @@ services: environment: - SPRING_DATASOURCE_USERNAME=root - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/BankDB?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC - - SPRING_DATASOURCE_PASSWORD=Test@123 + # Security: credentials sourced from .env file or environment variables instead of hardcoded values + - SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD:?Set SPRING_DATASOURCE_PASSWORD in .env} ports: - "8080:8080" depends_on: @@ -43,4 +46,4 @@ networks: bankapp: volumes: - bankapp-volume: \ No newline at end of file + bankapp-volume: diff --git a/helm/bankapp/templates/deployment.yml b/helm/bankapp/templates/deployment.yml index 9501ac23..dce47c68 100644 --- a/helm/bankapp/templates/deployment.yml +++ b/helm/bankapp/templates/deployment.yml @@ -15,9 +15,13 @@ spec: labels: app: {{ .Values.app_deployment.name }} spec: + # Security: use dedicated SA with least-privilege; disable API token mount + serviceAccountName: "{{ .Values.app_deployment.name }}-sa" + automountServiceAccountToken: false initContainers: - name: "wait-for-{{ .Values.db_statefulset.name }}" - image: busybox:1.28 + # Security: pin init container image to SHA digest for supply-chain integrity + image: busybox:1.28@sha256:141c253bc4c3fd0a201d32dc1f493bcf3fff003b6df416dea4f41046e0f37d47 command: ['sh', '-c', 'until nc -z {{ .Values.db_statefulset.name }}-0.{{ .Values.db_statefulset.name }}-headless 3306; do echo waiting for mysql; sleep 5; done;'] containers: - name: {{ .Values.app_deployment.name }} diff --git a/helm/bankapp/templates/mysqlStatefulSet.yml b/helm/bankapp/templates/mysqlStatefulSet.yml index ad094aa3..11554c94 100644 --- a/helm/bankapp/templates/mysqlStatefulSet.yml +++ b/helm/bankapp/templates/mysqlStatefulSet.yml @@ -16,6 +16,9 @@ spec: labels: app: {{ .Values.db_statefulset.name }} spec: + # Security: use dedicated SA with least-privilege; disable API token mount + serviceAccountName: "{{ .Values.db_statefulset.name }}-sa" + automountServiceAccountToken: false containers: - name: {{ .Values.db_statefulset.name }} image: {{ .Values.image.db }} diff --git a/helm/bankapp/templates/networkPolicies.yml b/helm/bankapp/templates/networkPolicies.yml new file mode 100644 index 00000000..7ed74bd4 --- /dev/null +++ b/helm/bankapp/templates/networkPolicies.yml @@ -0,0 +1,102 @@ +# Security: default-deny all ingress and egress traffic in the namespace. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all + namespace: {{ default "bankapp-namespace" .Values.namespace }} +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +# Security: allow bankapp pods to receive traffic only from the ingress controller. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: "allow-ingress-to-{{ .Values.app_deployment.name }}" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +spec: + podSelector: + matchLabels: + app: {{ .Values.app_deployment.name }} + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + ports: + - protocol: TCP + port: 8080 +--- +# Security: bankapp egress — allow only MySQL and DNS. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: "{{ .Values.app_deployment.name }}-egress" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +spec: + podSelector: + matchLabels: + app: {{ .Values.app_deployment.name }} + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - podSelector: + matchLabels: + app: {{ .Values.db_statefulset.name }} + ports: + - protocol: TCP + port: 3306 +--- +# Security: MySQL ingress — allow only from bankapp pods on port 3306. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: "allow-{{ .Values.app_deployment.name }}-to-{{ .Values.db_statefulset.name }}" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +spec: + podSelector: + matchLabels: + app: {{ .Values.db_statefulset.name }} + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: {{ .Values.app_deployment.name }} + ports: + - protocol: TCP + port: 3306 +--- +# Security: MySQL egress — DNS only. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: "{{ .Values.db_statefulset.name }}-egress" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +spec: + podSelector: + matchLabels: + app: {{ .Values.db_statefulset.name }} + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 diff --git a/helm/bankapp/templates/rbac.yml b/helm/bankapp/templates/rbac.yml new file mode 100644 index 00000000..14260b7f --- /dev/null +++ b/helm/bankapp/templates/rbac.yml @@ -0,0 +1,72 @@ +# Security: dedicated ServiceAccount for the bankapp deployment. +# Pods should not use the default SA, which may carry cluster-wide permissions. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: "{{ .Values.app_deployment.name }}-sa" + namespace: {{ default "bankapp-namespace" .Values.namespace }} + labels: + app: {{ .Values.app_deployment.name }} +automountServiceAccountToken: false +--- +# Security: dedicated ServiceAccount for MySQL. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: "{{ .Values.db_statefulset.name }}-sa" + namespace: {{ default "bankapp-namespace" .Values.namespace }} + labels: + app: {{ .Values.db_statefulset.name }} +automountServiceAccountToken: false +--- +# Security: namespace-scoped Role for bankapp — read-only ConfigMaps/Secrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: "{{ .Values.app_deployment.name }}-role" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +rules: +- apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] +--- +# Security: bind the Role to bankapp SA (namespace-scoped, not ClusterRoleBinding). +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "{{ .Values.app_deployment.name }}-rolebinding" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +subjects: +- kind: ServiceAccount + name: "{{ .Values.app_deployment.name }}-sa" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +roleRef: + kind: Role + name: "{{ .Values.app_deployment.name }}-role" + apiGroup: rbac.authorization.k8s.io +--- +# Security: namespace-scoped Role for MySQL — read-only ConfigMaps/Secrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: "{{ .Values.db_statefulset.name }}-role" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +rules: +- apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get"] +--- +# Security: bind the Role to MySQL SA (namespace-scoped). +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: "{{ .Values.db_statefulset.name }}-rolebinding" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +subjects: +- kind: ServiceAccount + name: "{{ .Values.db_statefulset.name }}-sa" + namespace: {{ default "bankapp-namespace" .Values.namespace }} +roleRef: + kind: Role + name: "{{ .Values.db_statefulset.name }}-role" + apiGroup: rbac.authorization.k8s.io diff --git a/helm/bankapp/values.yaml b/helm/bankapp/values.yaml index 58cf8483..47946978 100644 --- a/helm/bankapp/values.yaml +++ b/helm/bankapp/values.yaml @@ -22,9 +22,10 @@ app_deployment: mem_limit: 700Mi ## image repository and tag of app and db. +## Security: pin images to SHA digests to prevent tag-mutation supply-chain attacks image: app: trainwithshubham/springboot-bankapp:latest - db: mysql:latest + db: mysql:8.0@sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b # NodePort svc for bankapp bankapp_svc: @@ -44,11 +45,12 @@ hpa: max_replica: 5 cpu_utilizatoion: 40 -# Secret for Database connectivity - +# Security: in production, use an external secrets operator (e.g., AWS Secrets Manager, +# HashiCorp Vault, or Sealed Secrets) instead of storing credentials in values.yaml. +# Override these placeholder values via --set or a values-override file at deploy time. secret: name: mysql-secret - data: - MYSQL_ROOT_PASSWORD: Test@123 # (Base64 encoded 'Test@123') - SPRING_DATASOURCE_PASSWORD: Test@123 # (Base64 encoded 'Test@123') + data: + MYSQL_ROOT_PASSWORD: CHANGEME + SPRING_DATASOURCE_PASSWORD: CHANGEME diff --git a/kubernetes/bankapp-deployment.yml b/kubernetes/bankapp-deployment.yml index 45a35b6f..a4bb810a 100644 --- a/kubernetes/bankapp-deployment.yml +++ b/kubernetes/bankapp-deployment.yml @@ -15,6 +15,9 @@ spec: labels: app: bankapp-deploy spec: + # Security: use dedicated SA with least-privilege; disable API token mount + serviceAccountName: bankapp-sa + automountServiceAccountToken: false containers: - name: bankapp image: trainwithshubham/bankapp-eks:v2 diff --git a/kubernetes/mysql-deployment.yml b/kubernetes/mysql-deployment.yml index c9baa53a..be031b77 100644 --- a/kubernetes/mysql-deployment.yml +++ b/kubernetes/mysql-deployment.yml @@ -15,9 +15,13 @@ spec: labels: app: mysql spec: + # Security: use dedicated SA with least-privilege; disable API token mount + serviceAccountName: mysql-sa + automountServiceAccountToken: false containers: - name: mysql - image: mysql:8.0 # Use a specific, stable version for production + # Security: pin image to SHA digest for supply-chain integrity + image: mysql:8.0@sha256:7dcddc01f13bab2f15cde676d44d01f61fc9f99fe7785e86196dfc07d358ae2b ports: - containerPort: 3306 env: diff --git a/kubernetes/network-policies.yaml b/kubernetes/network-policies.yaml new file mode 100644 index 00000000..ac36d8ed --- /dev/null +++ b/kubernetes/network-policies.yaml @@ -0,0 +1,110 @@ +# Security: default-deny all ingress and egress traffic in the namespace. +# Only explicitly allowed paths (defined below) will be permitted. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all + namespace: bankapp-namespace +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +# Security: allow bankapp pods to receive traffic only from the ingress controller. +# This prevents direct pod-to-pod access from unauthorized sources. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-to-bankapp + namespace: bankapp-namespace +spec: + podSelector: + matchLabels: + app: bankapp-deploy + policyTypes: + - Ingress + ingress: + - from: + # Allow traffic from the NGINX ingress controller namespace + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + ports: + - protocol: TCP + port: 8080 +--- +# Security: allow bankapp pods to connect only to MySQL (port 3306) and DNS. +# Egress is restricted to prevent data exfiltration or lateral movement. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: bankapp-egress + namespace: bankapp-namespace +spec: + podSelector: + matchLabels: + app: bankapp-deploy + policyTypes: + - Egress + egress: + # Allow DNS resolution (required for service discovery) + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # Allow connection to MySQL within the namespace + - to: + - podSelector: + matchLabels: + app: mysql + ports: + - protocol: TCP + port: 3306 +--- +# Security: MySQL should only accept connections from bankapp pods. +# No other pod or external source should reach the database directly. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-bankapp-to-mysql + namespace: bankapp-namespace +spec: + podSelector: + matchLabels: + app: mysql + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: bankapp-deploy + ports: + - protocol: TCP + port: 3306 +--- +# Security: MySQL egress — allow only DNS for internal resolution. +# The database should not initiate outbound connections. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: mysql-egress + namespace: bankapp-namespace +spec: + podSelector: + matchLabels: + app: mysql + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 diff --git a/kubernetes/rbac.yaml b/kubernetes/rbac.yaml new file mode 100644 index 00000000..373a4d7b --- /dev/null +++ b/kubernetes/rbac.yaml @@ -0,0 +1,78 @@ +# Security: dedicated ServiceAccount for the bankapp deployment. +# Pods should not use the default SA, which may carry cluster-wide permissions +# granted by addon controllers or prior admin actions. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: bankapp-sa + namespace: bankapp-namespace + labels: + app: bankapp +# Security: disable automatic token mounting; the app does not call the K8s API +automountServiceAccountToken: false +--- +# Security: dedicated ServiceAccount for the MySQL deployment. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mysql-sa + namespace: bankapp-namespace + labels: + app: mysql +automountServiceAccountToken: false +--- +# Security: namespace-scoped Role granting only the minimum permissions the +# bankapp pods need (read-only access to ConfigMaps and Secrets in its namespace). +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: bankapp-role + namespace: bankapp-namespace +rules: +- apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] +--- +# Security: bind the bankapp Role to its ServiceAccount within the namespace. +# Using a RoleBinding (not ClusterRoleBinding) ensures the permission is scoped +# to bankapp-namespace only, following least-privilege. +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: bankapp-rolebinding + namespace: bankapp-namespace +subjects: +- kind: ServiceAccount + name: bankapp-sa + namespace: bankapp-namespace +roleRef: + kind: Role + name: bankapp-role + apiGroup: rbac.authorization.k8s.io +--- +# Security: namespace-scoped Role for MySQL — read-only access to its own +# ConfigMaps and Secrets for credential injection. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: mysql-role + namespace: bankapp-namespace +rules: +- apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get"] +--- +# Security: bind the mysql Role to its ServiceAccount within the namespace. +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: mysql-rolebinding + namespace: bankapp-namespace +subjects: +- kind: ServiceAccount + name: mysql-sa + namespace: bankapp-namespace +roleRef: + kind: Role + name: mysql-role + apiGroup: rbac.authorization.k8s.io diff --git a/kubernetes/secrets.yaml b/kubernetes/secrets.yaml index c6596fdb..fb21e430 100644 --- a/kubernetes/secrets.yaml +++ b/kubernetes/secrets.yaml @@ -1,10 +1,14 @@ +# Security: secret values should be managed via an external secrets operator +# (e.g., AWS Secrets Manager, HashiCorp Vault, or Sealed Secrets) in production. +# The base64 values below are placeholders for local/dev environments only. apiVersion: v1 kind: Secret metadata: name: mysql-secret namespace: bankapp-namespace + labels: + app: bankapp type: Opaque data: - MYSQL_ROOT_PASSWORD: VGVzdEAxMjM= # Base64 for "Test@123" - SPRING_DATASOURCE_PASSWORD: VGVzdEAxMjM= # Base64 for "Test@123" - + MYSQL_ROOT_PASSWORD: VGVzdEAxMjM= + SPRING_DATASOURCE_PASSWORD: VGVzdEAxMjM= diff --git a/pom.xml b/pom.xml index fc5bfeac..dfe1125b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,8 @@ org.springframework.boot spring-boot-starter-parent - 3.3.3 + + 3.5.15 com.example @@ -51,10 +52,11 @@ thymeleaf-extras-springsecurity6 + - mysql - mysql-connector-java - 8.0.33 + com.mysql + mysql-connector-j runtime @@ -75,15 +77,9 @@ org.springframework.boot spring-boot-maven-plugin - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.0 - - 1.8 - 1.8 - - + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 08663a63..7ecae864 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,11 +1,12 @@ spring.application.name=bankapp # MySQL Database configuration spring.datasource.url=jdbc:mysql://localhost:3306/bankappdb?useSSL=false&serverTimezone=UTC -spring.datasource.username=root -spring.datasource.password=Test@123 +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:root} +# Security: credential injected via environment variable; never hardcode secrets in source +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # JPA & Hibernate configuration spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect spring.jpa.show-sql=true