DevSecOps integrates security practices directly into the software development and delivery lifecycle rather than treating security as a final gate before production. The “shift-left” principle means catching vulnerabilities as early as possible — ideally at the developer’s workstation — where they are cheapest to fix. This tutorial walks through building a complete DevSecOps pipeline on RHEL 9, covering each stage from code commit to runtime, using open-source tools at every security gate.
Prerequisites
- RHEL 9 server with root or sudo access
- Git, Docker or Podman, and Python 3 installed
- Jenkins or access to GitHub Actions for the CI/CD orchestrator
- A Kubernetes cluster (local kind/minikube acceptable) for the deploy and runtime stages
- Basic familiarity with CI/CD concepts, Docker images, and Kubernetes
Step 1 — Stage 1: Code — Pre-commit Secret Scanning with TruffleHog
The first security gate runs before code even reaches the remote repository. Git pre-commit hooks intercept commits on the developer’s machine and block them if secrets are detected.
# Install TruffleHog v3
pip3 install trufflehog
# Install the pre-commit framework
pip3 install pre-commit
# Create a .pre-commit-config.yaml in the repo root
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: local
hooks:
- id: trufflehog
name: TruffleHog Secret Scan
entry: trufflehog git file://. --since-commit HEAD --only-verified --fail
language: system
pass_filenames: false
stages: [commit]
EOF
# Install the hook into the repo
pre-commit install
# Test it manually
pre-commit run trufflehog --all-files
TruffleHog v3 uses regex and entropy analysis to detect API keys, passwords, and tokens. The --only-verified flag reduces false positives by only flagging secrets that can be confirmed against known service endpoints.
Step 2 — Stage 2: Build — SAST with SonarQube
Static Application Security Testing (SAST) analyses source code for vulnerabilities without executing it. SonarQube Community Edition is free and self-hostable. Add a scanner invocation to your Jenkinsfile or GitHub Actions workflow:
# Install the SonarQube scanner CLI on RHEL 9
dnf install -y java-17-openjdk-headless unzip
curl -O https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip
unzip sonar-scanner-cli-5.0.1.3006-linux.zip -d /opt/
ln -s /opt/sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner /usr/local/bin/sonar-scanner
# Run a scan (set SONAR_HOST_URL and SONAR_TOKEN as environment variables)
sonar-scanner
-Dsonar.projectKey=myapp
-Dsonar.sources=./src
-Dsonar.host.url=http://sonarqube.internal:9000
-Dsonar.login="${SONAR_TOKEN}"
In a Jenkinsfile pipeline stage:
stage('SAST — SonarQube') {
steps {
withSonarQubeEnv('SonarQube') {
sh 'sonar-scanner -Dsonar.projectKey=myapp -Dsonar.sources=./src'
}
}
post {
always {
waitForQualityGate abortPipeline: true
}
}
}
Step 3 — Stage 3: Container Build — Image Scanning with Trivy
After building a container image, scan it for known CVEs in OS packages and application dependencies before pushing it to a registry:
# Install Trivy on RHEL 9
cat > /etc/yum.repos.d/trivy.repo << 'EOF'
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$releasever/$basearch/
gpgcheck=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
enabled=1
EOF
dnf install -y trivy
# Build the application image
podman build -t myapp:latest .
# Scan for HIGH and CRITICAL vulnerabilities; fail the pipeline if any are found
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latest
# Output in JUnit XML format for Jenkins test result ingestion
trivy image --format template --template "@/usr/local/share/trivy/templates/junit.tpl"
-o trivy-results.xml myapp:latest
Setting --exit-code 1 causes Trivy to return a non-zero exit code when vulnerabilities are found, which Jenkins and GitHub Actions interpret as a pipeline failure, blocking the push.
Step 4 — Stage 4: Infrastructure — IaC Scanning with Checkov
If your pipeline provisions infrastructure with Terraform or deploys Kubernetes manifests, scan them for misconfigurations before they reach a cluster:
# Install Checkov
pip3 install checkov
# Scan Terraform configuration in the infra/ directory
checkov -d ./infra --framework terraform --output junitxml > checkov-tf.xml
# Scan Kubernetes manifests
checkov -d ./k8s --framework kubernetes --output junitxml > checkov-k8s.xml
# Scan Dockerfile
checkov -f Dockerfile --framework dockerfile
# Fail on any HIGH severity findings
checkov -d ./infra --check HIGH --compact
Checkov catches issues such as S3 buckets without encryption, security groups open to the internet (0.0.0.0/0), containers running as root, and missing resource limits in Kubernetes manifests.
Step 5 — Stage 5: Deploy — Kubernetes Admission Control with OPA Gatekeeper
OPA Gatekeeper enforces policy as code on every Kubernetes admission request, rejecting workloads that violate security constraints before they are scheduled:
# Install Gatekeeper (requires kubectl and cluster access)
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.14/deploy/gatekeeper.yaml
# Create a ConstraintTemplate that blocks containers running as root
cat > no-root-constraint-template.yaml << 'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8snoroot
spec:
crd:
spec:
names:
kind: K8sNoRoot
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snoroot
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.runAsNonRoot != true
msg := sprintf("Container %v must not run as root", [container.name])
}
EOF
kubectl apply -f no-root-constraint-template.yaml
Step 6 — Stage 6: Runtime — Threat Detection with Falco
Falco monitors running containers and system calls at the kernel level, alerting on anomalous behaviour such as shell spawned in a container, file writes to sensitive paths, or privilege escalation attempts:
# Install Falco on RHEL 9
rpm --import https://falco.org/repo/falcosecurity-packages.asc
cat > /etc/yum.repos.d/falcosecurity.repo << 'EOF'
[falcosecurity]
name=falcosecurity
baseurl=https://download.falco.org/packages/rpm/
enabled=1
gpgcheck=1
gpgkey=https://falco.org/repo/falcosecurity-packages.asc
EOF
dnf install -y falco
# Start Falco — it will load its default ruleset
systemctl enable --now falco
# Tail Falco alerts in real time
journalctl -u falco -f
# Test: spawn a shell in a running container and observe the alert
podman exec -it myapp /bin/bash
# Falco will generate: "Terminal shell in container (user=root ...)"
Falco rules are written in YAML and can be customised or extended. Alerts can be forwarded to a SIEM via Falco’s output channels (gRPC, HTTP webhook, stdout, file). In a complete pipeline, Falco alerts feed back into an incident response workflow, closing the security loop from development to production.
Conclusion
You have built a complete DevSecOps pipeline on RHEL 9 spanning six stages: secret detection at commit time with TruffleHog, SAST in the build phase with SonarQube, container vulnerability scanning with Trivy, IaC misconfiguration detection with Checkov, Kubernetes admission policy enforcement with OPA Gatekeeper, and runtime threat detection with Falco. Each stage acts as an automated security gate that blocks insecure artefacts from progressing further in the pipeline, embodying the shift-left philosophy.
Next steps: How to Install and Configure Envoy Proxy on RHEL 9, How to Harden a RHEL 9 Server with OpenSCAP and the CIS Benchmark, and How to Set Up Centralised Logging with the ELK Stack on RHEL 9.