DevOps from Zero to Hero: Security Hardening

2026-06-11 | Gabriel Garrido | 20 min read
Share:

Support this blog

If you find this content useful, consider supporting the blog.

Introduction

Welcome to article eighteen of the DevOps from Zero to Hero series. Over the past seventeen articles we have built an application, tested it, containerized it, deployed it to Kubernetes, set up GitOps with ArgoCD, added observability, and assembled a complete CI/CD pipeline. Everything works. But there is a question we have been skirting around the whole time: is any of this secure?


Security is not a feature you bolt on at the end. It is a practice you weave into every layer of your pipeline, your infrastructure, and your daily habits. The good news is that you do not need to be a security expert to get the basics right. Most real-world breaches come from simple mistakes: leaked credentials, unpatched dependencies, containers running as root, overly permissive access. These are all preventable with a checklist and some automation.


This article is intentionally a beginner-friendly checklist, not a deep dive. If you want comprehensive coverage of topics like OPA Gatekeeper, Falco, or policy-as-code frameworks, check out the SRE Security as Code article. For a thorough walk-through of Kubernetes RBAC at the API level, see the RBAC Deep Dive. Here we are going to focus on the practical things every project should do from day one.


Let’s get into it.


The shift-left security mindset

The term “shift left” means moving security checks earlier in the development lifecycle. Instead of discovering a vulnerability in production (or worse, after a breach), you catch it during development or in your CI pipeline. The earlier you catch a problem, the cheaper and faster it is to fix.


Think of it like this. If you find a bug while writing code, it takes you five minutes to fix. If you find it in code review, it takes thirty minutes because you have to context-switch. If you find it in staging, it takes hours because now QA is involved. If you find it in production, it takes days and might involve an incident. Security issues follow the same curve, except the stakes are higher because a security issue can expose your users’ data.


Shifting left does not mean you stop doing security reviews in production. It means you add automated checks at every stage so that the obvious stuff never makes it that far. Your CI pipeline becomes your first line of defense.


The pipeline stages where security checks belong:


  • Code time: Linters, IDE plugins, pre-commit hooks that catch hardcoded secrets or insecure patterns before you even push
  • Pull request: SAST tools, dependency scanners, and secret detection run as CI checks on every PR
  • Build time: Container image scanning, SBOM generation, base image verification
  • Deploy time: Kubernetes admission controllers, Pod Security Standards, RBAC enforcement
  • Runtime: Network policies, audit logging, runtime threat detection (covered in the SRE series)

The rest of this article walks through each of these stages with practical examples you can add to your project today.


SAST: Static Application Security Testing

SAST tools analyze your source code without running it. They look for patterns that are known to cause security issues: SQL injection, cross-site scripting (XSS), command injection, insecure cryptography, hardcoded credentials, and more.


The key thing to understand is that SAST does not find every bug. It finds common patterns that match known vulnerability signatures. Think of it as a spell checker for security. It catches the obvious mistakes so you can focus your manual review time on the subtle ones.


Semgrep is one of the best tools for this. It is open source, supports many languages, and has a huge library of community rules. You can also write your own rules for patterns specific to your codebase.


Here is how to add Semgrep to your GitHub Actions pipeline:


# .github/workflows/security.yml
name: Security Checks

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  sast:
    name: SAST Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Semgrep
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/secrets
            p/owasp-top-ten
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

The p/security-audit, p/secrets, and p/owasp-top-ten are rule packs that cover the most common vulnerability patterns. Semgrep will scan your code and report any matches as comments on your pull request.


For JavaScript and TypeScript projects, you should also add ESLint security plugins:


npm install --save-dev eslint-plugin-security eslint-plugin-no-secrets

{
  "plugins": ["security", "no-secrets"],
  "extends": ["plugin:security/recommended"],
  "rules": {
    "no-secrets/no-secrets": "error",
    "security/detect-eval-with-expression": "error",
    "security/detect-non-literal-fs-filename": "warn",
    "security/detect-possible-timing-attacks": "warn"
  }
}

These plugins run as part of your normal linting step, so they catch issues before code even gets to the PR stage.


Dependency scanning

Your application code is probably 10% of the code that actually runs. The other 90% comes from dependencies. And those dependencies have their own dependencies (transitive dependencies). A vulnerability in a deeply nested transitive dependency can be just as dangerous as one in your own code.


This is not theoretical. The Log4Shell vulnerability (CVE-2021-44228) was in a logging library that was a transitive dependency in thousands of Java applications. Most teams did not even know they were using it until the CVE dropped.


npm audit is the simplest starting point for Node.js projects:


# Check for known vulnerabilities
npm audit

# Fix automatically where possible
npm audit fix

# Fail CI if there are high or critical vulnerabilities
npm audit --audit-level=high

Add this to your CI pipeline:


  dependency-scan:
    name: Dependency Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: npm ci

      - name: Run npm audit
        run: npm audit --audit-level=high

Dependabot is built into GitHub and automatically creates pull requests when new vulnerability patches are available. Enable it by adding a configuration file:


# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    reviewers:
      - "your-team"

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Notice that we are scanning three ecosystems: npm packages, Docker base images, and GitHub Actions versions. Each one is a potential attack surface.


Snyk is another popular option that provides deeper analysis than npm audit, including fix suggestions and prioritization based on exploitability. It has a free tier for open source projects.


The key habit here is: treat dependency updates as security maintenance, not optional chores. When Dependabot opens a PR, review it and merge it promptly. Stale dependencies are one of the most common attack vectors.


Container image scanning with Trivy

Your Docker images contain an entire operating system plus your application and its dependencies. Every package in that OS is a potential vulnerability. Trivy is an open-source scanner that checks your container images (and your Dockerfiles, and your Kubernetes manifests) for known vulnerabilities.


First, scan your Dockerfile for misconfigurations:


# Install Trivy
brew install trivy  # macOS
# or: sudo apt-get install trivy  # Ubuntu

# Scan a Dockerfile for misconfigurations
trivy config Dockerfile

# Scan a built image for vulnerabilities
trivy image myapp:latest

# Only show high and critical vulnerabilities
trivy image --severity HIGH,CRITICAL myapp:latest

# Fail if any critical vulnerabilities are found (useful for CI)
trivy image --severity CRITICAL --exit-code 1 myapp:latest

Common issues Trivy catches in Dockerfiles:


  • Running as root: Your container should use a non-root user. Add USER nonroot to your Dockerfile.
  • Using latest tag: Always pin your base image to a specific version or digest.
  • Missing health checks: Add a HEALTHCHECK instruction so orchestrators know when your app is unhealthy.
  • Sensitive data in layers: Never COPY secrets into your image. Use build args or mount secrets at runtime.

Here is a CI job that scans your image after building it:


  image-scan:
    name: Container Image Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy scan
        uses: aquasecurity/[email protected]
        with:
          image-ref: myapp:${{ github.sha }}
          format: table
          exit-code: 1
          severity: HIGH,CRITICAL
          ignore-unfixed: true

The ignore-unfixed: true flag skips vulnerabilities that do not have a fix available yet. This prevents your pipeline from blocking on issues you cannot actually resolve. You should still track unfixed vulnerabilities, but they should not break your build.


OIDC for CI/CD authentication

If your GitHub Actions workflows deploy to AWS (or any cloud provider), you need credentials. The old way was to store long-lived access keys as GitHub Secrets. The problem is that those keys never expire, they exist in multiple places, and if they leak, an attacker has persistent access to your AWS account.


OIDC (OpenID Connect) solves this by letting GitHub Actions request short-lived credentials directly from AWS. No long-lived keys stored anywhere. The credentials last for the duration of the workflow run and then they expire.


Here is how to set it up:


Step 1: Create an OIDC identity provider in AWS


# Create the OIDC provider (one-time setup)
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1" \
  --client-id-list "sts.amazonaws.com"

Step 2: Create an IAM role with a trust policy


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::ACCOUNT_ID::oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

The Condition block is important. It restricts which repository and branch can assume this role. Without it, any GitHub repository could use your AWS credentials.


Step 3: Use OIDC in your workflow


jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # Required for OIDC
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT_ID::role/github-actions-deploy
          aws-region: us-east-1

      - name: Deploy
        run: |
          # These credentials are short-lived and scoped to this workflow run
          aws sts get-caller-identity
          # ... your deployment commands

The permissions.id-token: write line is what enables OIDC. Without it, the workflow cannot request a token from GitHub’s OIDC provider.


This pattern works with AWS, GCP, Azure, and any cloud provider that supports OIDC. If your provider supports it, there is no reason to use long-lived keys.


Kubernetes RBAC basics

RBAC (Role-Based Access Control) controls who can do what in your Kubernetes cluster. The principle is simple: every user, service, and automation should have the minimum permissions it needs to do its job, and nothing more.


RBAC has four key resources:


  • Role: Defines a set of permissions within a namespace. For example, “can read pods and services in the staging namespace.”
  • ClusterRole: Same as Role but applies across the entire cluster. Use this for cluster-wide resources like nodes or namespaces.
  • RoleBinding: Connects a Role to a user, group, or ServiceAccount within a namespace.
  • ClusterRoleBinding: Connects a ClusterRole to a subject across the entire cluster.

Here is a basic example that gives a CI/CD ServiceAccount permission to manage deployments in a specific namespace:


# Create a ServiceAccount for your CI/CD pipeline
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-deployer
  namespace: production
---
# Define what it can do
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployer-role
  namespace: production
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "update", "patch"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
# Bind the role to the ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-deployer-binding
  namespace: production
subjects:
  - kind: ServiceAccount
    name: ci-deployer
    namespace: production
roleRef:
  kind: Role
  name: deployer-role
  apiGroup: rbac.authorization.k8s.io

Notice that the Role only grants get, list, update, and patch on deployments. It does not grant create or delete. It also does not grant access to secrets, configmaps, or any other resource. This is the principle of least privilege in action.


Common mistakes to avoid:


  • Using cluster-admin for everything: The cluster-admin ClusterRole gives full access to everything. Never bind it to service accounts used by applications or CI pipelines.
  • Using default ServiceAccounts: Every namespace has a default ServiceAccount. If you do not create specific ones, all your pods share the same identity. Create dedicated ServiceAccounts for each application.
  • Not auditing RBAC: Run kubectl auth can-i --list --as=system:serviceaccount:production:ci-deployer to verify what a ServiceAccount can actually do.

For a much deeper exploration of RBAC, including how it works at the HTTP API level with raw curl calls, check out the RBAC Deep Dive.


Network Policies

By default, every pod in a Kubernetes cluster can talk to every other pod. This is convenient for development but terrible for security. If an attacker compromises one pod, they can reach every other service in the cluster.


Network Policies let you control which pods can communicate with which other pods. Think of them as firewall rules for your cluster’s internal network.


Step 1: Start with a default deny policy


This blocks all ingress traffic to pods in the namespace. Nothing can talk to anything unless you explicitly allow it.


apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}    # Applies to all pods in the namespace
  policyTypes:
    - Ingress

Step 2: Allow specific traffic paths


Now you poke holes for the traffic that needs to flow. For example, let the API receive traffic from the ingress controller:


apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-to-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-system
        - podSelector:
            matchLabels:
              app: ingress-controller
      ports:
        - port: 3000
          protocol: TCP

And let the API talk to the database:


apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api
      ports:
        - port: 5432
          protocol: TCP

The pattern is always the same: deny everything by default, then allow only the specific paths your application needs. Document these paths. If you cannot explain why a network policy exists, it probably should not.


Important note: Network Policies require a CNI plugin that supports them. If you are using EKS, the default VPC CNI does not enforce Network Policies. You need to enable the Network Policy feature or use a CNI like Calico. Check your cluster’s CNI documentation.


Pod Security Standards

Pod Security Standards (PSS) define three profiles that control what pods are allowed to do at the security level:


  • Privileged: No restrictions. Use this only for system-level pods like CNI plugins or storage drivers that genuinely need host access.
  • Baseline: Prevents the most dangerous configurations like running as privileged, using host networking, or mounting the host filesystem. This is a reasonable default for most workloads.
  • Restricted: The strictest profile. Requires running as non-root, drops all Linux capabilities, sets a read-only root filesystem, and more. This is what production applications should target.

The simplest way to enforce these is with namespace labels. Kubernetes has a built-in admission controller called Pod Security Admission that reads these labels and enforces the corresponding profile.


apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    # Enforce: reject pods that violate the restricted profile
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest

    # Warn: log a warning for pods that violate restricted
    # (useful during migration to see what would break)
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest

    # Audit: add an audit annotation for baseline violations
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest

When you apply the enforce: restricted label, Kubernetes will reject any pod that does not meet the restricted profile. For example, if your pod spec does not include runAsNonRoot: true, the pod will be rejected at admission time.


Here is what a pod spec looks like when it meets the restricted profile:


apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: production
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myapp:1.0.0@sha256:abc123...
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
      resources:
        limits:
          memory: "128Mi"
          cpu: "500m"
        requests:
          memory: "64Mi"
          cpu: "250m"

If you are migrating existing workloads, start with warn mode to see what would fail, fix the violations, and then switch to enforce. Do not jump straight to enforce on a production namespace unless you have tested every workload.


Secrets hygiene

Secrets management is covered in depth in the Secrets and Config article from this series. Here we are going to focus on the hygiene practices that prevent secrets from leaking in the first place.


Never log secrets. This sounds obvious, but it happens all the time. A debug log statement prints the entire request object, which includes the Authorization header. A startup script echoes environment variables to verify configuration. An error handler dumps the full context, including database connection strings. All of these end up in your logging system, which is usually accessible to far more people than should have access to your secrets.


Practical rules:


  • Redact sensitive fields in logging: Configure your logging library to redact fields like password, token, secret, authorization, and cookie. Most logging libraries support this.
  • Never echo secrets in CI logs: If your CI pipeline needs a secret, use masked variables. GitHub Actions masks secrets automatically, but only if you reference them through ${{ secrets.NAME }}. If you copy the value to a regular variable and echo it, the masking does not apply.
  • Rotate secrets regularly: Set a rotation schedule. At minimum, rotate every 90 days. Rotate immediately if someone leaves the team or if you suspect a leak.
  • Audit who has access: Periodically review who can read your secrets. In Kubernetes, check which ServiceAccounts have get or list on secrets. In GitHub, review who has access to repository secrets.
  • Use short-lived tokens: Whenever possible, use tokens that expire. OIDC tokens, JWTs with short expiry, temporary AWS credentials. Long-lived tokens are a liability.

A quick way to check for hardcoded secrets in your codebase before they get committed:


# Install gitleaks
brew install gitleaks  # macOS

# Scan the current repo
gitleaks detect --source . --verbose

# Add as a pre-commit hook
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

Supply chain security

Supply chain attacks target the tools and dependencies you use rather than your code directly. The SolarWinds attack, the Codecov breach, and the ua-parser-js npm hijack are all examples. You cannot eliminate supply chain risk entirely, but you can reduce your exposure significantly.


Pin action versions by SHA, not tag


GitHub Actions tags are mutable. A malicious actor who compromises a popular action’s repository can update the v4 tag to point to malicious code, and every workflow using actions/checkout@v4 would run it. Pinning by SHA makes your workflow reproducible and tamper-resistant:


# Instead of this (tag can be moved):
- uses: actions/checkout@v4

# Use this (SHA is immutable):
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

Yes, it is less readable. Add a comment with the version number. Security is worth the trade-off.


Verify base images


Use official images from trusted registries. Pin them by digest, not just by tag:


# Instead of this (tag can be overwritten):
FROM node:20-alpine

# Use this (digest is content-addressable and immutable):
FROM node:20-alpine@sha256:abcdef1234567890...

You can find the digest on Docker Hub or by running docker inspect --format='{{index .RepoDigests 0}}' node:20-alpine.


SBOM (Software Bill of Materials)


An SBOM is an inventory of every component in your application. When a new CVE drops, an SBOM tells you immediately whether you are affected. You do not have to go digging through node_modules or Docker layers.


Trivy can generate SBOMs:


# Generate an SBOM in SPDX format
trivy image --format spdx-json --output sbom.json myapp:latest

# Generate in CycloneDX format
trivy image --format cyclonedx --output sbom.xml myapp:latest

Store your SBOM as a build artifact in your CI pipeline so you can reference it later when new vulnerabilities are disclosed.


For a more comprehensive treatment of supply chain security including Cosign image signing and Kyverno policies, see the SRE Security as Code article.


The practical security checklist

Here is a top-10 list of things every project should implement. These are ordered roughly by impact and effort, so start from the top and work your way down.


  • 1. Enable Dependabot or equivalent: Turn on automated dependency updates for all your ecosystems (npm, Docker, GitHub Actions). This takes five minutes and catches most known vulnerabilities automatically.
  • 2. Add secret scanning to your repo: Enable GitHub secret scanning or add gitleaks as a pre-commit hook. This prevents accidental credential leaks, which are the number one cause of breaches in small teams.
  • 3. Scan container images in CI: Add Trivy or a similar scanner to your build pipeline. Fail the build on critical vulnerabilities. This catches OS-level vulnerabilities that your language-level scanners miss.
  • 4. Use OIDC instead of long-lived keys: If your CI deploys to a cloud provider, switch to OIDC authentication. Remove any long-lived access keys from your GitHub Secrets.
  • 5. Run containers as non-root: Update your Dockerfiles to use a non-root user. Apply Pod Security Standards at the namespace level to enforce this cluster-wide.
  • 6. Implement network policies: Start with default-deny and explicitly allow the traffic your application needs. This limits blast radius if a pod gets compromised.
  • 7. Create dedicated RBAC roles: Stop using cluster-admin and default ServiceAccounts. Create specific Roles with minimum permissions for each workload and CI pipeline.
  • 8. Add SAST to your CI pipeline: Add Semgrep or equivalent SAST tooling. Even the default rule packs catch a surprising number of real issues.
  • 9. Pin your dependencies: Pin action versions by SHA, pin base images by digest, and use lock files for package managers. This protects against supply chain attacks.
  • 10. Rotate secrets on a schedule: Set calendar reminders to rotate API keys, database passwords, and service account tokens every 90 days. Automate rotation where possible.

You do not need to do all ten in one sprint. Start with items 1 through 4. They are quick wins with high impact. Then work through the rest as you mature your security posture.


Putting it all together in CI

Here is a complete security workflow that combines several of the checks we discussed. You can add this alongside your existing CI pipeline:


# .github/workflows/security.yml
name: Security

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  schedule:
    # Run weekly even without code changes to catch new CVEs
    - cron: "0 8 * * 1"

jobs:
  sast:
    name: SAST
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

      - name: Semgrep
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/security-audit
            p/secrets
            p/owasp-top-ten

  dependencies:
    name: Dependency Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

      - name: Install dependencies
        run: npm ci

      - name: npm audit
        run: npm audit --audit-level=high

  image-scan:
    name: Image Scan
    runs-on: ubuntu-latest
    needs: [sast, dependencies]  # Only scan if code checks pass
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Trivy scan
        uses: aquasecurity/[email protected]
        with:
          image-ref: myapp:${{ github.sha }}
          format: table
          exit-code: 1
          severity: HIGH,CRITICAL
          ignore-unfixed: true

      - name: Generate SBOM
        if: github.ref == 'refs/heads/main'
        run: |
          trivy image --format spdx-json \
            --output sbom.json myapp:${{ github.sha }}

      - name: Upload SBOM
        if: github.ref == 'refs/heads/main'
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.json

A few things to notice:


  • Scheduled runs: The cron trigger runs the scan weekly even if no code changes. New CVEs are published constantly, and a dependency that was clean last week might have a critical vulnerability today.
  • Actions pinned by SHA: We practice what we preach. The checkout action is pinned to a specific commit.
  • SBOM as artifact: On main branch builds, we generate and store an SBOM so we have a record of exactly what went into each release.
  • Fail fast: The image scan only runs if SAST and dependency checks pass. No point scanning an image if the code itself has issues.

Closing notes

Security is not a project with a finish date. It is a practice, like testing or code review. The goal is not to make your system impenetrable (nothing is), but to make it hard enough that attackers move on to easier targets, and to limit the damage when something does get through.


In this article we covered the shift-left security mindset, SAST with Semgrep and ESLint plugins, dependency scanning with npm audit and Dependabot, container image scanning with Trivy, OIDC authentication for CI/CD, Kubernetes RBAC basics, network policies with default deny, Pod Security Standards and namespace enforcement, secrets hygiene practices, supply chain security with pinned versions and SBOMs, and a practical top-10 security checklist.


Every topic here was covered at the checklist level. If you want to go deeper, the SRE Security as Code article covers OPA Gatekeeper, Falco runtime security, Cosign image signing, and policy-as-code frameworks. The RBAC Deep Dive article walks through RBAC at the Kubernetes API level with raw HTTP calls.


Start with the checklist. Pick the top three or four items that your project is missing and implement them this week. Security is one of those things where doing something is infinitely better than doing nothing.


In the next article we will cover disaster recovery and backup strategies, the final layer of protection when everything else fails.


Hope you found this useful and enjoyed reading it, until next time!


Errata

If you spot any error or have any suggestion, please send me a message so it gets fixed.

Also, you can check the source code and changes in the sources here



$ Comments

Online: 0

Please sign in to be able to write comments.

2026-06-11 | Gabriel Garrido