DevOps from Zero to Hero: Security Hardening
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 nonrootto your Dockerfile.- Using latest tag: Always pin your base image to a specific version or digest.
- Missing health checks: Add a
HEALTHCHECKinstruction so orchestrators know when your app is unhealthy.- Sensitive data in layers: Never
COPYsecrets 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-adminClusterRole gives full access to everything. Never bind it to service accounts used by applications or CI pipelines.- Using default ServiceAccounts: Every namespace has a
defaultServiceAccount. 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-deployerto 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, andcookie. 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
getorliston 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
crontrigger 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: 0Please sign in to be able to write comments.