From 5c576d589d648533ce990126ce797109cc0990d5 Mon Sep 17 00:00:00 2001 From: Kuruyia <8174691+Kuruyia@users.noreply.github.com> Date: Sun, 25 Feb 2024 00:03:24 +0100 Subject: [PATCH] feat: improve infrastructure security (#75) * ci: update paths-filter action * ci: sign container images * chore: add Kyverno policy * chore: add user in backend Dockerfile * chore: add security context to deployments * chore: add resource limits to deployments * feat(kube): add network policies * ci: don't sign the image if not pushing --- .github/workflows/flow_backend_build_push.yml | 19 ++++++++ .github/workflows/on_push_pr_main.yml | 3 +- .github/workflows/on_semver_tag.yml | 1 + backend/Dockerfile | 2 + kube/base/api_gateway/deployment.yml | 8 ++++ kube/base/employer/deployment.yml | 8 ++++ kube/base/jobs/deployment.yml | 8 ++++ kube/base/messaging/deployment.yml | 8 ++++ kube/base/notification/deployment.yml | 8 ++++ kube/base/profile/deployment.yml | 8 ++++ kube/base/recommendation/deployment.yml | 8 ++++ kube/prod/add-resource-limits.patch.yml | 9 ++++ kube/prod/kustomization.yml | 15 ++++-- kube/prod/kyverno-policy.yml | 28 +++++++++++ kube/prod/network-policies.yml | 47 +++++++++++++++++++ 15 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 kube/prod/add-resource-limits.patch.yml create mode 100644 kube/prod/kyverno-policy.yml create mode 100644 kube/prod/network-policies.yml diff --git a/.github/workflows/flow_backend_build_push.yml b/.github/workflows/flow_backend_build_push.yml index c527c78..8c1bc0d 100644 --- a/.github/workflows/flow_backend_build_push.yml +++ b/.github/workflows/flow_backend_build_push.yml @@ -33,6 +33,10 @@ jobs: packages: write contents: read steps: + - name: Install Cosign + uses: sigstore/cosign-installer@v3 + if: ${{ inputs.push-image }} + - name: Setup Docker buildx uses: docker/setup-buildx-action@v3 @@ -53,6 +57,7 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v5 + id: build_and_push with: push: ${{ inputs.push-image }} tags: ${{ steps.meta.outputs.tags }} @@ -61,3 +66,17 @@ jobs: cache-to: type=gha,mode=max context: "{{ defaultContext }}:${{ inputs.service-build-context-path }}" build-args: SERVICE_NAME=${{ inputs.service-name }} + + - name: Sign image with Cosign + if: ${{ inputs.push-image }} + run: | + images="" + for tag in ${TAGS}; do + images+="${tag}@${DIGEST} " + done + cosign sign --yes --key env://COSIGN_PRIVATE_KEY ${images} + env: + TAGS: ${{ steps.meta.outputs.tags }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + DIGEST: ${{ steps.build_and_push.outputs.digest }} diff --git a/.github/workflows/on_push_pr_main.yml b/.github/workflows/on_push_pr_main.yml index af247dc..6a1c943 100644 --- a/.github/workflows/on_push_pr_main.yml +++ b/.github/workflows/on_push_pr_main.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v3 id: filter with: filters: | @@ -101,6 +101,7 @@ jobs: name: Build & push Docker image of the "${{ matrix.service }}" service needs: lint_backend uses: ./.github/workflows/flow_backend_build_push.yml + secrets: inherit strategy: matrix: service: [api_gateway, jobs, messaging, employer, notification, profile, recommendation] diff --git a/.github/workflows/on_semver_tag.yml b/.github/workflows/on_semver_tag.yml index 13f05a1..eb1c2b8 100644 --- a/.github/workflows/on_semver_tag.yml +++ b/.github/workflows/on_semver_tag.yml @@ -9,6 +9,7 @@ jobs: build_push_backend: name: Build & push Docker image of the "${{ matrix.service }}" service uses: ./.github/workflows/flow_backend_build_push.yml + secrets: inherit strategy: matrix: service: [api_gateway, jobs, messaging, employer, notification, profile, recommendation] diff --git a/backend/Dockerfile b/backend/Dockerfile index 0f50aa5..6e96478 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,8 +16,10 @@ RUN --mount=type=cache,target=/root/.gradle ./gradlew clean bootJar # Run stage FROM amazoncorretto:17-alpine AS run ARG SERVICE_NAME +RUN addgroup -S linkedout && adduser -u 1001 -S linkedout -G linkedout WORKDIR /app COPY --from=build /app/$SERVICE_NAME/build/libs/$SERVICE_NAME.jar ./app.jar +USER 1001 ENTRYPOINT ["java", "-jar", "./app.jar"] diff --git a/kube/base/api_gateway/deployment.yml b/kube/base/api_gateway/deployment.yml index df1f47d..1cae604 100644 --- a/kube/base/api_gateway/deployment.yml +++ b/kube/base/api_gateway/deployment.yml @@ -14,12 +14,20 @@ spec: labels: app.kubernetes.io/name: linkedout-apigw app.kubernetes.io/part-of: linkedout + network-group: api-gateway spec: containers: - name: linkedout-apigw image: ghcr.io/thomas-mauran/linkedout/api_gateway imagePullPolicy: Always env: [] + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 9090 name: api diff --git a/kube/base/employer/deployment.yml b/kube/base/employer/deployment.yml index 7a5392d..19716f3 100644 --- a/kube/base/employer/deployment.yml +++ b/kube/base/employer/deployment.yml @@ -14,12 +14,20 @@ spec: labels: app.kubernetes.io/name: linkedout-employer app.kubernetes.io/part-of: linkedout + network-group: microservices spec: containers: - name: linkedout-employer image: ghcr.io/thomas-mauran/linkedout/employer imagePullPolicy: Always env: [] + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 8083 name: api diff --git a/kube/base/jobs/deployment.yml b/kube/base/jobs/deployment.yml index 3e4c4bf..4e871fa 100644 --- a/kube/base/jobs/deployment.yml +++ b/kube/base/jobs/deployment.yml @@ -14,12 +14,20 @@ spec: labels: app.kubernetes.io/name: linkedout-jobs app.kubernetes.io/part-of: linkedout + network-group: microservices spec: containers: - name: linkedout-jobs image: ghcr.io/thomas-mauran/linkedout/jobs imagePullPolicy: Always env: [] + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 8081 name: api diff --git a/kube/base/messaging/deployment.yml b/kube/base/messaging/deployment.yml index 4b4fd29..632caf3 100644 --- a/kube/base/messaging/deployment.yml +++ b/kube/base/messaging/deployment.yml @@ -14,12 +14,20 @@ spec: labels: app.kubernetes.io/name: linkedout-messaging app.kubernetes.io/part-of: linkedout + network-group: microservices spec: containers: - name: linkedout-messaging image: ghcr.io/thomas-mauran/linkedout/messaging imagePullPolicy: Always env: [] + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 8082 name: api diff --git a/kube/base/notification/deployment.yml b/kube/base/notification/deployment.yml index bd30b95..c5d6525 100644 --- a/kube/base/notification/deployment.yml +++ b/kube/base/notification/deployment.yml @@ -14,12 +14,20 @@ spec: labels: app.kubernetes.io/name: linkedout-notification app.kubernetes.io/part-of: linkedout + network-group: microservices spec: containers: - name: linkedout-notification image: ghcr.io/thomas-mauran/linkedout/notification imagePullPolicy: Always env: [] + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 8084 name: api diff --git a/kube/base/profile/deployment.yml b/kube/base/profile/deployment.yml index af1de8b..c0ac3d9 100644 --- a/kube/base/profile/deployment.yml +++ b/kube/base/profile/deployment.yml @@ -14,12 +14,20 @@ spec: labels: app.kubernetes.io/name: linkedout-profile app.kubernetes.io/part-of: linkedout + network-group: microservices spec: containers: - name: linkedout-profile image: ghcr.io/thomas-mauran/linkedout/profile imagePullPolicy: Always env: [] + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 8085 name: api diff --git a/kube/base/recommendation/deployment.yml b/kube/base/recommendation/deployment.yml index c54ca83..6185320 100644 --- a/kube/base/recommendation/deployment.yml +++ b/kube/base/recommendation/deployment.yml @@ -14,12 +14,20 @@ spec: labels: app.kubernetes.io/name: linkedout-recommendation app.kubernetes.io/part-of: linkedout + network-group: microservices spec: containers: - name: linkedout-recommendation image: ghcr.io/thomas-mauran/linkedout/recommendation imagePullPolicy: Always env: [] + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL ports: - containerPort: 8086 name: api diff --git a/kube/prod/add-resource-limits.patch.yml b/kube/prod/add-resource-limits.patch.yml new file mode 100644 index 0000000..3f6caeb --- /dev/null +++ b/kube/prod/add-resource-limits.patch.yml @@ -0,0 +1,9 @@ +- op: add + path: /spec/template/spec/containers/0/resources + value: + requests: + memory: "1Gi" + cpu: "0.5" + limits: + memory: "2Gi" + cpu: "1" diff --git a/kube/prod/kustomization.yml b/kube/prod/kustomization.yml index d4dc927..619173e 100644 --- a/kube/prod/kustomization.yml +++ b/kube/prod/kustomization.yml @@ -4,18 +4,25 @@ kind: Kustomization namespace: linkedout resources: - ../base +- kyverno-policy.yml +- network-policies.yml patches: - target: - group: networking.k8s.io + group: apps version: v1 - kind: Ingress - path: api_gateway/configure-ingress.patch.yml + kind: Deployment + path: add-nats.patch.yml - target: group: apps version: v1 kind: Deployment - path: add-nats.patch.yml + path: add-resource-limits.patch.yml +- target: + group: networking.k8s.io + version: v1 + kind: Ingress + path: api_gateway/configure-ingress.patch.yml - target: group: apps version: v1 diff --git a/kube/prod/kyverno-policy.yml b/kube/prod/kyverno-policy.yml new file mode 100644 index 0000000..fcc99bb --- /dev/null +++ b/kube/prod/kyverno-policy.yml @@ -0,0 +1,28 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: check-linkedout-image-signature +spec: + validationFailureAction: Enforce + background: false + webhookTimeoutSeconds: 30 + failurePolicy: Fail + rules: + - name: check-image + match: + any: + - resources: + kinds: + - Pod + verifyImages: + - imageReferences: + - "ghcr.io/thomas-mauran/linkedout/*" + attestors: + - count: 1 + entries: + - keys: + publicKeys: |- + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaFU86MNnoiHTLXkBrgnPO18R5gpo + cMic199RKzUa6YftDcDCEovrR0nyzfGp3pKcr4nhjwi3qNRQRHPz76EaWg== + -----END PUBLIC KEY----- diff --git a/kube/prod/network-policies.yml b/kube/prod/network-policies.yml new file mode 100644 index 0000000..e7fe180 --- /dev/null +++ b/kube/prod/network-policies.yml @@ -0,0 +1,47 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: microservices +spec: + podSelector: + matchLabels: + network-group: microservices + policyTypes: + - Ingress + - Egress + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: nats + - podSelector: + matchLabels: + app.kubernetes.io/name: postgresql + - podSelector: + matchLabels: + helm.neo4j.com/neo4j.name: linkedout + - podSelector: + matchLabels: + app.kubernetes.io/name: minio +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: coredns-egress +spec: + podSelector: + matchLabels: + network-group: microservices + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53