Deploying a simple application security lab
I have been looking for the opportunity for document deployment of a small application security lab that I created last year in my home. I want to be able to run SAST SCA and other application tools. To see how they behave and how they work with the CI/CD. I will be using Kubernetes for this project. Rationale is that Kubernetes allows IAC to deploy these containers. Which makes the deployment a lot more easier and simpler than having to deploy it from on host, dealing with dependencies and other “FUN” configurations. I will add detailed configuration steps later on how everything connects.
Documentation used for this:
- https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
- https://kubernetes.io/docs/concepts/workloads/controllers/job/
- https://kubernetes.io/docs/concepts/storage/persistent-volumes/
- https://kubernetes.io/docs/concepts/services-networking/service/
- https://www.jenkins.io/doc/book/pipeline/
Applications to be use:
- Jenkins
- GITLeaks
- SonarQube
- Dependency-Check (OWASP)
- JuiceShop (OWASP)
- DefectDojo (OWASP)
Deploying Jenkins#
Jenkins is a popular CI/CD automation server that lets you build, test, and deploy code automatically. A lot of software development companies have it but unfortunately they do not have it configured or have not played enough to utilize the full features that the application provides.
First let create 3 yml files. These files will provide the persistent volume, a service NodePort object that will allow us connect to the application and the deployment.
- jenkins-pvc.yaml -> will contain the configuration for the persistent volume.
- jenkins-deployment.yaml -> will contain the configuration for the deployment.
- jenkins-service.yaml -> will contain the NodePort Service to allow connections to the device.
- jenkins-rbac.yaml -> This will create a service account, Role and Rolebinding that will contain rules to allow Jenkins to connect to kubernetes and deploy pods on demand.
For deploying it we will use the command kubectl apply -f <file.yaml>.
Here are my yaml files that I used for the project.
jenkins-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
jenkins-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-jenkins
spec:
replicas: 1
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
serviceAccountName: jenkins
containers:
- name: jenkins
image: jenkins/jenkins:lts
ports:
- containerPort: 8080
volumeMounts:
- name: jenkins-data
mountPath: /var/jenkins_home
volumes:
- name: jenkins-data
persistentVolumeClaim:
claimName: jenkins-pvc
jenkins-service.yaml
apiVersion: v1
kind: Service
metadata:
name: jenkins
spec:
type: NodePort
selector:
app: jenkins
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 31000
- name: agent
port: 50000
targetPort: 50000
jenkins-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jenkins
namespace: default
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "pods/exec"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jenkins
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
namespace: default
With this done, you should have a fully functional Jenkins server that will maintain the configuration, as you can see in my screenshot below:

[tip] To get the initial password for Jenkins just run the following:
kubectl exec pod -it -- cat /var/jenkins_home/secrets/initialAdminPassword
Deploying SonarQube#
SonarQube is a SAST tool that scans source code for quality issues, bugs, code smells, and security vulnerabilities.
In the same fashion on how we deployed Jenkins this is how I will be deploying the application. Create three yaml files to hold the IAC of the application.
- sonarqube-pvc.yaml -> will contain the configuration for the persistent volume. In this one we will have three volumes to work with.
- sonarqube-deployment.yaml -> will contain the configuration for the deployment.
- sonarqube-service.yaml -> will contain the NodePort Service to allow connections to the device.
sonarqube-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-extensions
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-logs
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
sonarqube-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-sonarqube
spec:
replicas: 1
selector:
matchLabels:
app: sonarqube
template:
metadata:
labels:
app: sonarqube
spec:
containers:
- name: sonarqube
image: sonarqube:lts-community
ports:
- containerPort: 9000
env:
- name: SONAR_ES_BOOTSTRAP_CHECKS_DISABLE
value: "true"
volumeMounts:
- name: sonarqube-data
mountPath: /opt/sonarqube/data
- name: sonarqube-extensions
mountPath: /opt/sonarqube/extensions
- name: sonarqube-logs
mountPath: /opt/sonarqube/logs
volumes:
- name: sonarqube-data
persistentVolumeClaim:
claimName: sonarqube-data
- name: sonarqube-extensions
persistentVolumeClaim:
claimName: sonarqube-extensions
- name: sonarqube-logs
persistentVolumeClaim:
claimName: sonarqube-logs
sonarqube-service.yaml
apiVersion: v1
kind: Service
metadata:
name: sonarqube
spec:
type: NodePort
selector:
app: sonarqube
ports:
- port: 9000
targetPort: 9000
nodePort: 31001
Creating a Git-Leaks pipeline#
Gitleaks allow the scanning of hardcoded passwords or secrets in the submitted code. I am going to have jenkins create a new Pod in Kubernetes to have the pod run when its needed. This is the main reason we created that Service Account, Role and Role Binding for Jenkins earlier. Before creating the pipeline you must create an Cloud item in Jenkins for Kubernetes.
test-gitleaks pipeline
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: gitleaks
image: zricethezav/gitleaks:latest
command:
- sleep
args:
- 99d
'''
}
}
stages {
stage('Secret Scan') {
steps {
container('gitleaks') {
sh 'gitleaks detect --source=. --report-format=json --report-path=gitleaks-report.json --no-git || true'
}
archiveArtifacts artifacts: 'gitleaks-report.json', allowEmptyArchive: true
}
}
}
}
Creating a OWASP Dependency-Check pipeline#
OWASP Dependency-Check is a software component scanner that will let you know of vulnerable dependencies on the code. This is simpler as all you need is to create a pipeline.
[tip] Note that when this runs for the first time it will download the vulnerability database from nist.
You might recieve the warning:
[WARN] An NVD API Key was not provided - it is highly recommended to use an NVD API key as the update can take a VERY long time without an API Key
If you want you can navigate to https://nvd.nist.gov/developers/request-an-api-key fill out the form and get a key. You can add this to Jenkins to make the download faster.
Here is my pipeline script I created to test. As you can see I am also sending my report to my SonarQube. SonarQube will receive the XML read it, import the findings and display them in the dash board.
pipeline {
agent any
stages {
stage('Dependency Check') {
steps {
dependencyCheck additionalArguments: '--scan ./ --format HTML --format XML', odcInstallation: 'OWASP SCA'
dependencyCheckPublisher pattern: 'dependency-check-report.xml'
}
}
stage('SonarQube Analysis') {
steps {
script {
def scannerHome = tool 'SonarScanner'
withSonarQubeEnv('sonarqube') {
sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectKey=test-app -Dsonar.sources=. -Dsonar.dependencyCheck.reportPath=dependency-check-report.xml"
}
}
}
}
}
}
Deploying OWASP Juice Shop#
Juice Shop is a vulnerable web application created by OWASP for training and testing security tools. For this we will deploy in Kubernetes for ease. No PVC is needed but we will need to create two yaml files to help for the deployment. We will create a deployment and a service items.
- juiceshop-deployment.yaml -> will provide the deployment configuration.
- juiceshop-service.yaml -> will provice the service network configuration to be use
Here are the configurations I am using for my setup:
juiceshop-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: juiceshop
spec:
replicas: 1
selector:
matchLabels:
app: juiceshop
template:
metadata:
labels:
app: juiceshop
spec:
containers:
- name: juiceshop
image: bkimminich/juice-shop
ports:
- containerPort: 3000
juiceshop-service.yaml
apiVersion: v1
kind: Service
metadata:
name: juiceshop
spec:
type: NodePort
selector:
app: juiceshop
ports:
- port: 3000
targetPort: 3000
nodePort: 31002
Creating a ZAP pipeline#
OWASP ZAP (Zed Attack Proxy) is a free, open-source DAST (Dynamic Application Security Testing) tool. ZAP provides runtime security testing by simulating attacks against deployed applications, finding vulnerabilities that only appear when the app is actually running (like authentication bypasses, session management issues, etc.). This pipeline will run a scan against the OWASP Juice shop and provide the reports.
ZAP Pipeline
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: zap
image: ghcr.io/zaproxy/zaproxy:stable
command:
- sleep
args:
- 99d
'''
}
}
stages {
stage('DAST Scan') {
steps {
container('zap') {
sh '''
zap-baseline.sh -t http://juiceshop:3000 -r zap-report.html || true
'''
}
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'zap-report.html',
reportName: 'ZAP DAST Report'
])
}
}
}
}
Deploy DefectDojo#
DefectDojo is a security orchestration and vulnerability management tool it provides centralized vulnerability management across your entire security toolchain, giving security and development teams a single source of truth for all security findings and their remediation status.
Just the same as before we will create one service and run the helm command to install the rest of the application:
helm install defectdojo defectdojo/defectdojo --set redis.auth.enabled=false --set postgresql.auth.postgresPassword=defectdojo --set createSecret=true --set createPostgresqlSecret=true
- defectdojo-service.yaml -> contains all the configuration to allow the service to be reachable.
Here are my configurations:
defectdojo-service.yaml
apiVersion: v1
kind: Service
metadata:
name: defectdojo-external
spec:
type: NodePort
selector:
app.kubernetes.io/name: defectdojo
app.kubernetes.io/component: django
ports:
- port: 8080
targetPort: 8080
nodePort: 31003
Integrating tools in to Jenkins#
Integrating these tools with Jenkins is quite simple. The first item in the books will be adding the needed credentials. I will be using GitHub as my VCS. For this test we will be using secret text. Please note that is recommended to use GitHub App. But for the sake of training we will be using secret text.
Setting the credentials in GitHub.#
- Navigate and log on to GitHub.
- Go to Settings.
- Go to the bottom of settings and click on “Developer Settings.”
- For this scenario click on Personal access tokens and click on Fine-Grained tokens.
- Click generate token.
- Fill the form with the requested data.
- Make sure to add an expiration date prior clicking generate token.
- For repository permissions I will be using the “Read access to code, metadata, and pull requests” option and “Read and Write access to commit statuses and repository hooks”.
- Click Generate token.
- Save the generated token.
Setting the credentials in SonarQube.#
- Navigate and log on to SonarQube.
- Once in the dashboard click on your profile picture (top right of the Screen) then click “My Account”.
- Go to security tab.
- For this scenario we will be using a global token.
- Generate a new token.
- Save the token.
Setting OWASP Dependency-Check on Jenkins.#
- Log on to Jenkins
- Navigate to Manage Jenkins -> Plugins
- Click on Available plugins.
- Search for “OWASP Dependency-Check”
- Click the check box for install and click installed. Note: There might be need to restart the Jenkins server after the installation.
GetNVD API Keys (Recommended but not needed)#
While recommended not necessary. These keys will allow faster downloads from the NVD database. If not a scan can take up to 30+ minutes.
- Navigate to: https://nvd.nist.gov/developers/request-an-api-key
- Request free API key
- Check email for key
- In Jenkins: Manage Jenkins -> System
- Find Dependency-Check section
- Paste API key
- Click Save
Setting the credentials in Jenkins.#
Lets add the GitHub credentials.
- Navigate and log on to Jenkins
- Go to Manage Jenkins -> Credentials
- Click on System (global)
- Click on “Add Credentials”
- Select Secret Text/
- Add the secret token that you save from GitHub.
- Add an ID and description for ease of use.
Adding SonarQube.
- Navigate and log on to Jenkins
- Go to Manage Jenkins -> Credentials
- Click on System (global)
- Click on “Add Credentials”
- Select Secret Text/
- Add the secret token that you save from SonarQube.
- Add an ID and description for ease of use.
Configuring SonarQube#
- Navigate and log on to Jenkins.
- Go to Manage Jenkins -> System
- Scroll down until you see SonarQube Servers. If the item is not there you will need to download the SonarQube Plugin. From the Jenkins Marketplace.
- Add the name of what you want to call this server setup.
- Add the server URL.
- Lastly Server authentication token.
- Click Save.
Sample Jenkinsfile for triggering the builds:#
Here is a sample Jenkinsfile I created for scanning and testing a WebGoat fork. I use this to make sure that the applications are properly funtioning.
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers: - name: gitleaks image: zricethezav/gitleaks:latest command: - sleep args: - 99d - name: maven image: maven:3.9-eclipse-temurin-21 command: - sleep args: - 99d'''
}
}
environment {
APP_NAME = 'WebGoat'
SONARQUBE_SERVER = 'sonarqube'
BUILD_TIMESTAMP = sh(script: "date '+%Y-%m-%d_%H-%M-%S'", returnStdout: true).trim()
}
stages {
stage('Checkout') {
steps {
echo 'STAGE: Checkout Source Code'
echo "Branch: ${env.BRANCH_NAME}"
echo "Build: ${env.BUILD_NUMBER}"
sh '''
echo "Working on Directory: $(pwd)" echo "Git Status" git status echo "Git Log (last commit)" git log -1 ''' }
}
stage('Secret Scanning') {
steps {
echo "Stage: Secret Scanning"
container('gitleaks') {
script {
def exitCode = sh(
script: 'gitleaks detect --source=. --report-format=json --report-path=gitleaks-report.json --no-git',
returnStatus: true
)
if (exitCode == 0) {
echo "No secrets found"
} else if (exitCode == 1) {
error 'SECRET DETECTED! Check gitleaks-report.json'
} else {
error "Gitleaks failed with exit code: ${exitCode}"
}
}
}
}
post {
always {
archiveArtifacts artifacts: 'gitleaks-report.json', allowEmptyArchive: true
}
}
}
stage('Build') {
steps {
container('maven') {
echo 'Stage: Building Application'
//sh '''
// mvn clean package -DskipTests //''' //Trying to fix issue what will not allow the build to continue. sh '''
mvn clean package -DskipTests -Dmaven.compiler.release=21 ''' echo 'Build completed'
}
}
}
stage('Unit Test') {
steps {
container('maven') {
echo 'Stage: Running Unit Test'
sh '''
mvn test ''' echo 'Unit test passed'
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}
stage('Parallel Scans') {
parallel {
stage('SCA - Dependency Check') {
steps {
container('maven') {
echo "Parallel Stage: SCA"
dependencyCheck additionalArguments: '--scan ./ --format HTML --format XML', odcInstallation: 'OWASP SCA'
dependencyCheckPublisher pattern: 'dependency-check-report.xml'
}
}
}
stage('SAST - SonarQube') {
steps {
container('maven') {
echo "Parallel Stage: SAST"
script {
withSonarQubeEnv("${SONARQUBE_SERVER}") {
sh '''
mvn sonar:sonar \ -Dsonar.projectKey=${APP_NAME} \ -Dsonar.projectName=${APP_NAME} \ -Dsonar.java.binaries=target/classes ''' }
echo "SAST scan completed"
}
}
}
}
}
}
stage('Quality Gate') {
steps {
echo "Stage: Waiting for SonarQube Quality Gate"
script {
timeout(time: 5, unit: 'MINUTES') {
def qg = waitForQualityGate()
if (qg.status != 'OK') {
error "Quality Gate failed: ${qg.status}"
}
}
}
echo 'Quality Gate passed'
}
}
stage('Package') {
steps {
container('maven') {
echo "Stage: Package Application"
sh '''
echo "Packaging WebGoat Jar...." ls -lh target/*.jar ''' archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
echo "Package completed"
}
}
}
stage('Deploy to Dev') {
steps {
echo "Stage: Deploy to Development Environment"
// Not doing anything just wanted to have this one to look good in the Jenkins job
}
}
}
post {
always {
echo "Pipeline completed"
cleanWs()
}
success {
echo "Pipeline completed successfully."
}
failure {
echo "Pipeline Failed"
}
}
}