Quantcast
Channel: Oracle SOA / Java blog
Viewing all 142 articles
Browse latest View live

Blocking vs non-blocking in a Spring stack: R2DBC vs JDBC and WebFlux vs Web MVC

$
0
0
Spring Framework version 5, released in Sept 2017, introduced Spring WebFlux. A fully reactive stack. In Dec 2019 Spring Data R2DBC, a reactive relational database driver was released. What are the performance benefits or drawbacks of using non-blocking stacks/drivers compared to their blocking alternatives?



Method

In this blog post I've looked at 4 implementations
  • Spring Web MVC + JDBC database driver
  • Spring Web MVC + R2DBC database driver
  • Spring WebFlux + JDBC database driver
  • Spring WebFlux + R2DBC database driver
I've varied the number of requests in progress (concurrency) from 4 to 500 in steps of 50 and assigned 4 cores to the load generator and to the service (my laptop has 12 cores). I've configured all connection pools to be 100. Why a fixed number of cores and connection pool size? In a previous exploration of JDBC vs R2DBC data changing those variables did not provide much additional insight so I decided to keep them fixed for this test reducing my test run time by several factors.

I did a GET request on the service. The service fetched 10 records from the database and returned them as JSON. First I 'primed' the service for 2 seconds by putting heavy load on the service. Next I started with a 1 minute benchmark. I repeated every scenario 5 times (separated by other tests, so not 5 times after each other) and averaged the results. I only looked at the runs which did not cause errors. When I increased concurrency to more than 1000, the additional concurrent requests failed without exception for all implementations. The results appeared reproducible.

As back end database I've used Postgres (12.2). I used wrk to benchmark the implementations and measured
  • Process CPU usage
    User and kernel time (based on /proc/PID/stat)
  • Memory usage
    Private and shared process memory (based on /proc/PID/maps)
  • Response time
    As reported by wrk
  • Throughput (number of requests)
    As reported by wrk
You can view the test script used here.

Results

You can view the raw data which I used for the graphs here.

CPU

Concurrency on the x-axis and CPU usage on the y-axis 

Spring WebFlux used more CPU than Spring MVC and R2DBC used more CPU than JDBC. However when you look at the CPU used per request processed, you get a better idea why the non-blocking parts cause higher CPU usage:


WebFlux and R2DBC use less CPU per request than their blocking alternatives. As you will see later in the throughput graph, they are able to process more requests using the same amount of CPU resources!

Memory

Memory usage [Mb] on the y-axis and concurrency on the x-axis
Interesting to see is that memory usage for R2DBC seems to rise when concurrency is increased while this does not appear to be as clear in the other implementations.

Response time

Response time [ms] on the y-axis and concurrency on the x-axis
It is clear that at higher concurrency, the response times of Spring Web MVC + JDBC starts to drop. R2DBC clearly gives the better response times at higher concurrency. Spring WebFlux also does better than a similar implementation using Spring Web MVC.

Throughput


Number of requests which could be processed in 60 seconds on the y-axis and concurrency on the x-axis
Similar to the response times, Spring Web MVC with JDBC starts doing worse at higher concurrency. R2DBC clearly does best. Moving from Spring Web MVC to Spring WebFlux however also helps improving throughput but not as much as going from JDBC to R2DBC. At low concurrency, Spring Web MVC + JDBC does slightly better than Spring WebFlux + JDBC.

Summary

R2DBC and WebFlux, a good idea at high concurrency!

  • At high concurrency, the benefit of using R2DBC instead of JDBC is obvious. This allows your cores to be utilized more efficiently since less CPU is required to process a single request. Response times at high concurrency are also better when using R2DBC instead of JDBC.
  • WebFlux provides also several benefits when used instead of Web MVC such as stable and higher throughput at high concurrency.
  • You're not required to have a completely non-blocking stack to reap the benefits of using R2DBC or WebFlux.

Some challenges when using R2DBC

  • Be careful with your memory usage when using R2DBC, especially in combination with WebFlux.
  • JPA cannot deal with reactive repositories such as provided by Spring Data R2DBC. This means you will have to do more things manually when using R2DBC.
  • There are other reactive drivers around such as for example Quarkus Reactive Postgres client (which uses Vert.x). This does not use R2DBC and has different performance characteristics (see here).
  • Limited availability
    Not all relational databases have reactive drivers available. For example, Oracle does not (yet?) have an R2DBC implementation. 
  • Application servers still depend on JDBC.
    Do people still use those for non-legacy in this Kubernetes age?
  • When Java Fibers will be introduced (Project Loom, could be Java 15), the driver landscape might change again and R2DBC might not become JDBCs successor after all.

Quick and easy: A multi-node Kubernetes cluster on CentOS 7 + QEMU/KVM (libvirt)

$
0
0
Kubernetes is a popular container orchestration platform. As a developer understanding the environment in which your application is going to run is important since this can help you use available services of the platform and fix issues.

There are several options to run Kubernetes locally to get some experience with Kubernetes as developer. For example Minikube, MicroK8s and MiniShift. These options however are not representative for a real environment. They for example usually do not have master and slave nodes. Running locally requires quite different configuration compared to running multiple nodes in VMs. Think for example about how to deal with storage and a container registry which you want to share over the different nodes. Installing a full blown environment requires a lot of work and resources. Using a cloud service usually is not free and you usually have less to no control over the environment Kubernetes is running in.

In this blog I'll describe a 'middle way'. Get an easy to manage small multi node Kubernetes environment running in different VMs. You can use this environment for example to learn what the challenges of clusters are and how to deal with them efficiently.

It uses the work done here with some minor additions to get a dashboard ready.

Getting the host ready

As host OS I used Cent OS 7 (on bare metal). CentOS 8 introduces some major changes such as Podman instead of Docker so I did not want to take any risks and decided to stick with this commonly used open source OS compiled from Red Hat sources. I do recommend sticking to a single partition for a local development environment to make it more easy for yourself. Also I used a minimal desktop environment with administrative tools.

Also create a user which can do sudo to execute various commands in the following steps.

Install QEMU/KVM + libvirt

We are going to use QEMU/KVM and access it through libvirt. Why? Because I want to approach bare metal performance as much as I can and QEMU/KVM does a good job at that. See for example this performance comparison of bare metal vs KVM vs Virtualbox. KVM greatly outperforms Virtualbox and approaches bare metal speeds in quite some tests. I do like the Virtualbox GUI though but I can live with the Virtual Machine Manager.

The following will do the trick on CentOS 7

sudo yum install qemu-kvm qemu-img virt-manager libvirt libvirt-python libvirt-client virt-install virt-viewer bridge-utils gcc git make

Install Vagrant and required plugins

Vagrant is used to create the virtual machines for the master and nodes. Vagrant can easily be installed from here. It even has a CentOS specific RPM which is nice.

With Vagrant I'm going to use two plugins. vagrant-libvirt and vagrant-sshfs. The first plugin allows vagrant to manage QEMU/KVM VMs through libvirt. The second plugin will be used for shared folders. Why sshfs? Mainly because libvirt alternatives for shared folders such as NFS and 9p were more difficult to set-up and I wanted to be able to provide the same shared storage to all VMs.

vagrant plugin install vagrant-libvirt
vagrant plugin install vagrant-sshfs

Install Kubernetes

Install kubectl

First install kubectl on the host. This is described in detail here.

cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF
yum install -y kubectl


Create the VMs

Execute the following under a normal user (you also installed the Vagrant plugins under this user).

git clone https://github.com/MaartenSmeets/k8s-vagrant-multi-node.git
cd k8s-vagrant-multi-node
mkdir data/shared

make up -j 3 BOX_OS=centos VAGRANT_DEFAULT_PROVIDER=libvirt NODE_CPUS=1 NODE_COUNT=2 MASTER_CPUS=2

This process will take a while. You can follow the progress by looking in the Virtual Machine Manager and in the console.


The command will also ask a couple of times for the user password. Be ready to input this since if you wait too long, it will timeout and the build will fail. If it fails, clean up using

make clean VAGRANT_DEFAULT_PROVIDER=libvirt

If at the end you only see a single node, you can do the following to create a second node

make start-node-2 NODE_CPUS=1 VAGRANT_DEFAULT_PROVIDER=libvirt BOX_OS=centos

There are many more useful commands available in the Makefile so do check them out! Read the documentation here.

Check you can access the environment by:

kubectl get nodes

NAME     STATUS   ROLES    AGE   VERSION
master   Ready    master   15h   v1.18.2
node1    Ready    <none>   15h   v1.18.2
node2    Ready    <none>   15h   v1.18.2

Configure Kubernetes

We now have a master and two nodes running.

Install a dashboard

The environment does not have an out of the box dashboard like OKD (open source OpenShift). Even though the make scripts allow you to add the dashboard during the creation of the nodes, I prefer to this afterwards so I know what I'm doing.

kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0/aio/deploy/recommended.yaml

Now you have a dashboard but no user who can browse resources. In order to give an admin user the required privileges, I did the following (based on this).

cat > dashboard-adminuser.yaml << EOF
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kube-system
EOF
 

kubectl apply -f dashboard-adminuser.yaml

cat > admin-role-binding.yaml << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kube-system
EOF


kubectl apply -f admin-role-binding.yaml

Now you can obtain a token by:

kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')

Start a local proxy:

kubectl proxy

Now you can use the token to login to:

http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

and open the dashboard


Additional notes

This environment is not done yet.
  • Every node uses its own local registry. The Makefile used does provide ways to load the same image to the different registries but what I actually want is all the nodes to use the same registry.
  • There is no shared PersistentVolume available within the Kubernetes environment which can be used. Shared storage. Preparations for that have been made though since /shared in every VM is mounted to the same local folder.
  • We have not installed anything in the environment yet but a small dashboard. I want to have Jenkins running inside and be able to deploy applications.
Still some work to be done so stay tuned!

Charmed Kubernetes on KVM using MAAS and juju

$
0
0
Coming to this solution was a journey. I was looking for a Kubernetes installation which was easy to deploy and cleanup on my own laptop (I didn't want to have to pay for a hosted solution). I did want a solution which was more or less production like because I wanted to be able to play with storage solutions and deal with cluster challenges / loadbalancers. Things I couldn't do easily on environments like Minikube and Microk8s. Also, since I was running on a single piece of hardware, I needed a virtualization technology. Each one of them comes with their own challenges. On some of them it is difficult to get storage solutions to work, for example LXC/LXD (an alternative to Docker). Some of them come with networking challenges like Hyper-V and some of them just don't perform well like VirtualBox. I also needed a solution to provide some form of automation to create/destroy/access my virtual environments. A long list of requirements and this is what I ended up with. A production-like environment which is quick to create, destroy or reset, running on my laptop with easy management tools.


Tools

Charms

Charmed Kubernetes is a pure upstream distribution of Kubernetes by Canonical (you know, the people from Ubuntu). It can be deployed using so-called Charms with a tool called juju. Read more here. Charms consist of a definition of applications and their required resources. Using juju you can deploy charms to various environments such as AWS, Azure, OpenStack and GCP. You can also deploy locally to LXC/LXD containers or to a MaaS (Metal as a Service) solution. juju allows easy ssh access into nodes and several commands to manage the environment. In addition, it comes with a nice GUI.


Virtualization technology

During my first experiments I tried deploying locally to LXC/LXD. It looked nice at first since I was able to deploy an entire Kubernetes environment using very few commands and it was quick to start/stop/rebuild. When however I tried to do more complex things like deploying StorageOS, CephFS or OpenEBS, I failed miserably. After trying many things I found out I was mainly trying to get LXC/LXD do what I want. My focus wasn't getting to know LXC/LXD indebt but playing with Kubernetes. Since LXC/LXD isn't a full virtualization solution (it uses the same kernel as the host, similar to Docker) it was difficult to pull the required devices with the required privileges from the host into the LXC/LXD containers and from the containers into the containerd processes. This caused different issues with the storage solutions. I decided I needed a full virtualization solution which also virtualized the node OS, thus KVM or VirtualBox (I had no intention of running on Windows so dropped Hyper-V).


Deployment tooling

I already mentioned I wanted an easy way to manage the virtualized environments. I started out with Vagrant but the combination of getting a production-like environment out of the box using Vagrant scripts was hard to find. Also it would require me to define all the machines myself in Vagrant code. The Kubernetes distributions I would end up with were far from production like. They were automated mostly by developers who had similar requirements as my own but those solutions were not kept up to date by a big company and shared by many users. I wanted automation like juju but not LXC/LXD. What were my options without having to pay anything for it? MaaS, Metal as a Service, was something to try out. Juju could deploy to MaaS. I found that MaaS could be configured to create KVM machines on Juju's request! Nice, let's use it!


My main challenge was getting networking to work. MaaS can create virtual machines in KVM using virsh. The machines boot and then they have to be supplied with software to allow management and deployment of applications. MaaS does this by 'catching' machines on DHCP requests and then use PXE to provide the required software. This is all automated though. You only have to provide the networking part.

Bringing it all together

I started out with a clean Ubuntu 18.04 install (desktop, minimal GUI) on bare metal. I could also have chosen for 20.04 since that has also recently been released but I thought, let's not take any chances with the available tutorials by choosing a distribution which might contain changes which would make the tutorials fail. I think this can also be applied to 20.04 since it is not really that different compared to 18.04 but I haven't tried.

Some preparations

First I installed some packages required for the setup. I choose to use MaaS from packages instead of Snap since I couldn't get the connection to KVM to work with the Snap installation. The package installation created a user maas which was used to connect to the libvirt daemon to create VMs.

sudo apt-add-repository -yu ppa:maas/stable
sudo apt-get update
sudo apt-get -y install bridge-utils qemu-kvm qemu virt-manager net-tools openssh-server mlocate maas maas-region-controller maas-rack-controller


Configure the network

Probably the most difficult step. Part of the magic is done by using the following netplan configuration:

sudo bash
cat << EOF > /etc/netplan/01-netcfg.yaml
network:
  version: 2
  renderer: NetworkManager
  ethernets:
    dummy:
      dhcp4: false
  bridges:
    br0:
      interfaces: [dummy]
      dhcp4: false
      addresses: [10.20.81.2/24]
      gateway4: 10.20.81.254
      nameservers:
        addresses: [10.20.81.1]
      parameters:
        stp: false
EOF
netplan apply
exit


This creates a bridge interface/subnet which allows the VMs to communicate with each other and with the host. The stp parameter is needed to allow PXE (the network boot solution MaaS uses) to work.

The below part creates an interface which is available in KVM for the VMs to use. It is a bridge interface to the subnet. The below commands replace the default KVM interface.

sudo virsh net-destroy default 
sudo virsh net-undefine default

cat << EOF > maas.xml
<network> 
    <name>default</name> 
    <forward mode="bridge"/> 
    <bridge name="br0"/>
</network>
EOF

virsh net-define maas.xml

virsh net-autostart default
virsh net-start default


Since we're at it, KVM also needs to have some storage defined:

virsh pool-define-as default dir - - - - "/var/lib/libvirt/images" 
virsh pool-autostart default 
virsh pool-start default

In order to allow the hosts on the subnet (your KVM virtual machines) to communicate with the internet, some iptable rules are required. In the below script replace enp4s0f1 with your interface which is connected to the internet (you can determine it from the ifconfig output)

sudo iptables -t nat -A POSTROUTING -o enp4s0f1 -j MASQUERADE 
sudo iptables -A FORWARD -i enp4s0f1 -o br0 -m state --state RELATED,ESTABLISHED -j ACCEPT 
sudo iptables -A FORWARD -i br0 -o enp4s0f1 -j ACCEPT 
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf 
echo 'net.ipv4.conf.br0.proxy_arp=1' | sudo tee -a /etc/sysctl.conf 
echo 'net.ipv4.conf.br0.proxy_arp_pvlan' | sudo tee -a /etc/sysctl.conf

#For saving the iptables rules. After install the installer asks to save the current state
sudo apt-get install iptables-persistent

#If you want to save the state afterwards, do:
#sudo bash
#iptables-save > /etc/iptables/rules.v4

It is a good idea to do a reboot now to make sure the state of your network is reproducible. The sysctl code is executed at boot before the br0 bridge becomes available so after a reboot, you'll need to execute the below 3 lines. I do not have a solution for that yet.

sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv4.conf.br0.proxy_arp=1
sudo sysctl -w net.ipv4.conf.br0.proxy_arp_pvlan=1

Maas

Next step is setting up Maas.

sudo maas init

This will ask you for an admin user and you can also supply an SSH key. After you've answered the questions, you can use the user you have created to login to:

http://localhost:5240/MAAS

You need to enable DHCP for the subnet you've created (10.20.81.0/24)




Also the maas OS user needs to be able to manage KVM. Add the maas user to the libvirt group:

sudo usermod -a -G libvirt maas

Create a public and private key for the user maas and add the public key to the maas user s authorized keys so the private key can be used to login to the maas user:

sudo chsh -s /bin/bash maas
sudo su - maas
ssh-keygen -f ~/.ssh/id_rsa -N ''

cd .ssh
cat id_rsa.pub > authorized_keys
chmod 600 ~/.ssh/authorized_keys

The public key needs to be registered in maas

Click on admin in the webinterface and add the SSH public key


Now maas needs to be configured to connect to KVM. Click pods and add the virsh URL: qemu+ssh://maas@127.0.0.1/system


You also installed virt-manager so you can manually create a VM and check it becomes available in MaaS (does the PXE boot and starts provisioning).

Juju

Now you have the network, KVM, MaaS configured and it's time to move to the next step. Configure Juju.

#Install juju
snap install juju --classic

#Add your MaaS cloud environment
juju add-cloud --local

Use the following API endpoint:http://10.20.81.2:5240/MAAS. Do not use localhost or 127.0.0.1! This IP is also accessible from hosts on your subnet and allows juju to manage the different hosts.


Add credentials (the MaaS user). First obtain an API key from the MaaS GUI.


juju add-credential maas


Bootstrap JuJu. This creates a controller environment. Juju issues its commands through this. If this step succeeds, installing Charmed Kubernetes will very likely also work. Should this fail, you can issue: juju kill-controller to start again. Do not forget to cleanup the VM in MaaS and KVM in such a case.

juju bootstrap maas maas-controller


Installing Charmed Kubernetes

A full installation of Charmed Kubernetes requires quite some resources. Most likely you don have them available on your laptop. Resources like 16 cores and more than 32Gb of memory.

Charms contain a description of the applications and the resources required to run them so hosts can automatically be provisioned to accomodate for those applications. You can override the resource requirements by using a so-called overlay.

For example, the original Charm looks like this: https://api.jujucharms.com/charmstore/v5/bundle/canonical-kubernetes-899/archive/bundle.yaml

I reduced the number of workers, the number of cores available to the workers and the amount of memory used by the individual master and the worker nodes so the entire thing would fit on my laptop.

cat << EOF > bundle.yaml
description: A highly-available, production-grade Kubernetes cluster.
series: bionic
applications:
  kubernetes-master:
    constraints: cores=1 mem=3G root-disk=16G
    num_units: 2
  kubernetes-worker:
    constraints: cores=2 mem=6G root-disk=16G
    num_units: 2
EOF


Create a model to deploy Charmed Kubernetes in:

juju add-model k8s

Now execute the command which will take a while (~15 minutes on my laptop):

juju deploy charmed-kubernetes  --overlay bundle.yaml --trust

This command provisions machines using MAAS and deploys Charmed Kubernetes on top.

Some housekeeping

In order to access the environment, you can do the following after the deployment has completed (you can check with juju status or juju gui)

sudo snap install kubectl --classic
sudo snap install helm --classic

mkdir .kube
juju scp kubernetes-master/0:config ~/.kube/config

The resulting environment

After juju is done deploying Charmed Kubernetes you will end up with an environment which consists of 8 hosts (+1 juju controller). Below is the output of juju status after the deployment has completed.



You can also browse the juju GUI. 'juju gui' gives you the URL and credentials required to login.


You can look in the MAAS UI to see how your cores and memory have been claimed


In virt-manager you can also see the different hosts

'kubectl get nodes' gives you the worker nodes.


You can enter a VM by for example 'juju ssh kubernetes-master/0'

If this does not work because provisioning failed or the controller is not available, you can also use the juju private key directly

ssh -i /home/maarten/.local/share/juju/ssh/juju_id_rsa ubuntu@10.20.81.1

You can execute commands against all nodes which provide an application:

juju run "uname -a" kubernetes-master


Of course you can access the dashboard which is installed by default:

kubectl proxy

Next access http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/login

You can use the .kube/config to login



Cleaning up

If you need to cleanup, the best way to remove Charmed Kubernetes is:

juju destroy-model k8s

This will remove the application and the machines so you can start over if you have destroyed your Kubernetes environment. Reinstalling it can be done with the below two commands: 

juju add-model k8s   
juju deploy charmed-kubernetes --overlay bundle.yaml --trust 

If you want to go further back and even remove juju, I recommend the following procedure:

sudo snap remove juju --purge
sudo snap remove kubectl --purge
sudo snap remove helm --purge
rm -rf ~/.kube
rm -rf ~/.local

References

I've mainly used the following sites to put the pieces of the puzzle together

StorageOS: Create persistent storage in your Charmed Kubernetes cluster

$
0
0
If you want to experiment with a multi node Kubernetes cluster locally as a developer, you need a distributed persistent storage solution to approximate real production scenario's. StorageOS is one of those solutions. In this blog I describe a developer installation StorageOS. For production scenario's check out the best practices mentioned on the StorageOS site.


Introduction

The environment

I've used the following Kubernetes environment (Charmed Kubernetes deployed locally using Juju and MaaS on KVM) which is described here. This is a production-like environment consisting of several hosts. I had also tried getting StorageOS to work on the LXC/LXD install of Charmed Kubernetes but that failed.


The storage solution

For choosing storage solutions for Kubernetes there are various options available. For Charmed Kubernetes, CephFS is mentioned. However when using Charms, new hosts are created and I was out of cores on my laptop so I decided to go for a solution which was Charm independent. In addition StorageOS had a nice GUI and is free for developers with some limits.

StorageOS requirements

Kernel modules

StorageOS requires some modules to be loaded by the hosts running the containers. I was running Ubuntu 18.04 and used juju for provisioning so I did the following to get my kubernetes-worker nodes ready.

Install the required kernel modules


juju run "sudo apt -y update && sudo apt -y install linux-modules-extra-$(uname -r) && sudo apt-get clean" --application kubernetes-worker
 

Allow containers to run privileged

juju config kubernetes-master allow-privileged=true

Make sure the kernel modules load on startup

juju run "echo target_core_mod >> /etc/modules" --application kubernetes-worker
juju run "echo tcm_loop >> /etc/modules" --application kubernetes-worker
juju run "echo target_core_file >> /etc/modules" --application kubernetes-worker
juju run "echo configfs >> /etc/modules" --application kubernetes-worker
juju run "echo target_core_user >> /etc/modules" --application kubernetes-worker
juju run "echo uio >> /etc/modules" --application kubernetes-worker


Reboot the worker nodes (one after each other since we're running in a highly available environment)


juju run "reboot" --unit kubernetes-worker/0
juju run "reboot" --unit kubernetes-worker/1


Install etcd

StorageOS requires etcd to run. They recommend not to use the Kubernetes etcd here so we install a new one.

git clone https://github.com/coreos/etcd-operator.git
export ROLE_NAME=etcd-operator
export ROLE_BINDING_NAME=etcd-operator
export NAMESPACE=etcd
kubectl create namespace $NAMESPACE
./etcd-operator/example/rbac/create_role.sh

kubectl -n $NAMESPACE create -f - <<END
apiVersion: apps/v1
kind: Deployment
metadata:
  name: etcd-operator
spec:
  selector:
    matchLabels:
      app: etcd-operator
  replicas: 1
  template:
    metadata:
      labels:
        app: etcd-operator
    spec:
      containers:
      - name: etcd-operator
        image: quay.io/coreos/etcd-operator:v0.9.4
        command:
        - etcd-operator
        env:
        - name: MY_POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: MY_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
END

Now the etcd operator is deployed. We need to label our nodes for operator to allow creating pods on the right hosts.

Determine nodes: kubectl get nodes. In the below example
valued-calf and valid-thrush


kubectl label nodes valued-calf  etcd-cluster=storageos-etcd
kubectl label nodes valid-thrush  etcd-cluster=storageos-etcd


Create the etcd cluster

kubectl -n etcd create -f - <<END
apiVersion: "etcd.database.coreos.com/v1beta2"
kind: "EtcdCluster"
metadata:
 name: "storageos-etcd"
spec:
 size: 1
 version: "3.4.7"
 pod:
   etcdEnv:
   - name: ETCD_QUOTA_BACKEND_BYTES
     value: "2147483648"  # 2 GB
   - name: ETCD_AUTO_COMPACTION_RETENTION
     value: "100" # Keep 100 revisions
   - name: ETCD_AUTO_COMPACTION_MODE
     value: "revision" # Set the revision mode
   resources:
     requests:
       cpu: 200m
       memory: 300Mi
   securityContext:
     runAsNonRoot: true
     runAsUser: 9000
     fsGroup: 9000
   tolerations:
   - operator: "Exists"
END


Install StorageOS

Install the operator

kubectl create -f https://github.com/storageos/cluster-operator/releases/download/v2.0.0/storageos-operator.yaml

Create a secret

cat << EOF > storageos_secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: "storageos-api"
  namespace: "storageos-operator"
  labels:
    app: "storageos"
type: "kubernetes.io/storageos"
data:
  # echo -n '<secret>' | base64
  apiUsername: c3RvcmFnZW9z
  apiPassword: c3RvcmFnZW9z
  # CSI Credentials
  csiProvisionUsername: c3RvcmFnZW9z
  csiProvisionPassword: c3RvcmFnZW9z
  csiControllerPublishUsername: c3RvcmFnZW9z
  csiControllerPublishPassword: c3RvcmFnZW9z
  csiNodePublishUsername: c3RvcmFnZW9z
  csiNodePublishPassword: c3RvcmFnZW9z
EOF
kubectl apply -f storageos_secret.yaml


Create a storage cluster

kubectl create -f - <<END
apiVersion: storageos.com/v1
kind: StorageOSCluster
metadata:
 name: example-storageoscluster
 namespace: "storageos-operator"
spec:
 secretRefName: "storageos-api"
 secretRefNamespace: "storageos-operator"
 k8sDistro: "upstream"  # Set the Kubernetes distribution for your cluster (upstream, eks, aks, gke, rancher, dockeree)
 # storageClassName: fast # The storage class creates by the StorageOS operator is configurable
 csi:
   enable: true
   deploymentStrategy: "deployment"
   enableProvisionCreds: true
   enableControllerPublishCreds: true
   enableNodePublishCreds: true
 kvBackend:
   address: "storageos-etcd-client.etcd.svc.cluster.local:2379"
END

 

In order to use StorageOS for a longer time, you need to register. You can do this by opening the dashboard and creating an account. First determine where the GUI is running. Open the storageos service in the kube-system namespace.


Next determine the endpoints. The GUI is running on those. In my case on 10.20.81.46 and 10.20.81.47.


Open the GUI in your browser by opening http://endpointIP. You can login with user and password storageos

In the License screen (click it in the left menu) you can create an account. After you've confirmed your e-mail address, your developer license becomes active.


Testing it out

Create a persistent volume claim

kubectl create -f - <<END
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-pv-claim
spec:
  storageClassName: "fast" # StorageOS StorageClass
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
END


Install Jenkins

helm repo add stable https://kubernetes-charts.storage.googleapis.com/
helm repo update

cat << EOF > jenkins-config.yaml
persistence:
    enabled: true
    size: 5Gi
    accessMode: ReadWriteOnce
    existingClaim: jenkins-pv-claim
    storageClass: "fast"
EOF

helm install my-jenkins-release -f jenkins-config.yaml stable/jenkins

Get your 'admin' user password by running:

printf $(kubectl get secret --namespace default my-jenkins-release -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

Get the Jenkins URL to visit by running these commands in the same shell and login!

export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/component=jenkins-master" -l "app.kubernetes.io/instance=my-jenkins-release" -o jsonpath="{.items[0].metadata.name}")

kubectl --namespace default port-forward $POD_NAME 8080:8080



OpenEBS: Create persistent storage in your Charmed Kubernetes cluster

$
0
0
I previously wrote a blog about using StorageOS as persistent storage solution for Kubernetes here. StorageOS is dependent on etcd. I was having difficulties getting etcd up again after a reboot. Since I wanted to get a storage solution working quickly and not focus too much on external dependencies so I decided to give OpenEBS a try. In this blog I'll describe a developer installation on Charmed Kubernetes (the environment described here). I used openebs-jiva-default as storage class. This is unsuitable for production scenario's. OpenEBS also provides cStor. Most of the development effort goes there. cStor however requires a mounted block device. I have not tried this yet in my environment.

Installing OpenEBS

First I created my environment as described here. I used 4 worker nodes.


For the creation of the environment I used the following yaml file as overlay

description: A highly-available, production-grade Kubernetes cluster.
series: bionic
applications:
  etcd:
    num_units: 2
  kubernetes-master:
    constraints: cores=1 mem=4G root-disk=16G
    num_units: 2
  kubernetes-worker:
    constraints: cores=1 mem=3G root-disk=20G
    num_units: 4


Next I enabled iscsi as per OpenEBS requirement in the worker nodes.

juju run "sudo systemctl enable iscsid && sudo systemctl start iscsid" --application kubernetes-worker


Allow creation of privileged containers. The containers need access to host devices to do their thing.

juju config kubernetes-master allow-privileged=true


Create a namespace

kubectl create namespace openebs

Add the OpenEBS Helm chart

helm repo add openebs https://openebs.github.io/charts
helm repo update


Add some configuration for OpenEBS. Parameters are described here.

cat << EOF > openebs-config.yaml
jiva:
   replicas: 2
EOF


Install OpenEBS

helm install openebs stable/openebs --version 1.10.0 -f openebs-config.yaml --namespace openebs

Trying it out

Add the Jenkins repo

helm repo add stable https://kubernetes-charts.storage.googleapis.com/
helm repo update


Create a namespace

kubectl create namespace jenkins

Create a persistent volume claim

kubectl create -n jenkins -f - <<END
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-pv-claim
spec:
  storageClassName: openebs-jiva-default
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
END


Create some Jenkins configuration to use the claim

 cat << EOF > jenkins-config.yaml
persistence:
    enabled: true
    size: 5Gi
    accessMode: ReadWriteOnce
    existingClaim: jenkins-pv-claim
    storageClass: "openebs-jiva-default"
EOF


Install Jenkins

helm install my-jenkins-release -f jenkins-config.yaml stable/jenkins --namespace jenkins

Get your 'admin' user password by running:

printf $(kubectl get secret --namespace jenkins my-jenkins-release -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

Get the Jenkins URL to visit by running these commands in the same shell and login!

export POD_NAME=$(kubectl get pods --namespace jenkins -l "app.kubernetes.io/component=jenkins-master" -l "app.kubernetes.io/instance=my-jenkins-release" -o jsonpath="{.items[0].metadata.name}")

kubectl --namespace jenkins port-forward $POD_NAME 8080:8080




OBS Studio + Snap Camera: Putting yourself in your presentation live for free!

$
0
0
When giving online presentations, it helps for personal marketing if people can see you on the screen. Various tools provide features which help you achieve that, for example Microsoft Teams. Sometimes though you do not have that available or want to be able to do more than what the tool you are using provides. Using OBS Studio (free) with Snap Camera (free) or ChromaCam ($29.99 lifetime license) you can easily put yourself in your own presentations in such a way that it will work on almost any medium you would like to present on without having to invest in a green screen. Want to know how? Read on! 


Tools used
  • OBS Studio (here)
    OBS Studio is free open source software which allows you to do many interesting things to video
  • OBS VirtualCam (here)
    OBS VirtualCam is an OBS Studio plugin which allows you to output of OBS Studio to a virtual camera device. This device can be used as source camera in tools like Skype (the Desktop version, not the App!) or Teams
  • Snap Camera (here)
    Snap Camera can filter you out your background and replace it with whatever you like. ChromaCam (here) is a commercial alternative used for most screenshots in this blog post. It provides additional options and better quality background filtering. You can also use Snap Camera or ChromaCam to surprise colleagues in online meetings.
This setup has been created on Windows but parts of it are portable to other OSs. Snap Camera is also available for macOS just like ChromaCam. OBS Studio works on Windows, macOS and Linux. The OBS VirtualCam plugin is Windows only. I've tested this with Skype, Teams and YouTube Studio but I expect other broadcast/meeting software to also work.

Setup

First download and install OBS. Next install the OBS VirtualCam plugin. Next install Snap Camera. These are 'next, next, finish' installers so I will not go into detail here.

The below setup describes the use of Snap Camera. Using ChromaCam is similar but the quality is better (it is better able to distinguish you from the background) and it has some additional features. 

Snap Camera

Start Snap Camera and search for Green Screen. You can chose one of the plugins presented (they are similar in quality)



OBS Studio

Open OBS Studio and add under sources a Video Capture Device. Select the Snap Camera and accept the defaults.


Right-click the Video Capture Device and select Filters


Add the Chroma Key filter and set it to green. You can tweak the settings to get optimal results. It makes a specific color transparent in the image so you can put something underneath.


Presentation

Add a browser to the sources, put it in order below your Video Capture Device and interact with it by right clicking, interact.


Now you can open your presentation in your browser from your OneDrive folder in Windows and put it full screen in the browser window. Resize the layers so they will fit your broadcast. I did not manage to do this directly with Powerpoint, hence the browser trick. You can browse through slides using the interact screen which will not be broadcast. This works great when you only have a single screen available!


Broadcasting using VirtualCam

Using the OBS VirtualCam, your OBS output becomes available in other tools as an OBS-Camera.



Below you can see an example from Microsoft Teams. In the Custom Setup screen you can select your camera.


When using Skype mind that for Windows in order to use the OBS-Camera, you cannot use the Skype App but need the Skype Desktop application.


If the presentation is flipped, you can fix it by right clicking the Browser source, Transform, Flip Horizontal.


Broadcasting using YouTube Studio

Open YouTube Studio (here) and create a new stream. Mind not to make it for kids since that will limit your options.


Copy the Stream key


Input the stream key in OBS Studio, File, Settings, Stream.


Now start broadcasting (the button on the bottom right) and check the stream. Ready to go live! When you go live, you can share the link to the broadcast and people can tune-in, no specific client required. Only a browser.



Scanning images using Anchore Engine

$
0
0
Applications nowadays, are usually deployed inside containers. A container consists of libraries and tools which allow the application to run inside. It is not only important to keep your application up to date but also the environment in which it is deployed. How do you check if that environment, the container, is secure? There are various tools available to scan container images for security vulnerabilies. Having little experience with them, but recognizing the importance of having such a tool, I decided to give Anchore Engine a try. Why? Because it appeared popular when looking for tools, it has an open source variant which I can appreciate and it was very easy to get started with. In addition, it provides a Jenkins plugin and a Kubernetes Admission Controller.


Getting started

Anchore Engine provides various ways in which you can install it here. I decided to follow the Docker Compose quickstart instruction here. I will not repeat the entire quickstart since it is straightforward, but  provide a quick example:
 #Download and run docker-compose file  
curl https://docs.anchore.com/current/docs/engine/quickstart/docker-compose.yaml > docker-compose.yaml
docker-compose up -d

#Check status of feeds (first time updating can take a while)
docker-compose exec api anchore-cli system feeds list

#Block until complete
docker-compose exec api anchore-cli system wait

#Start analysis
docker-compose exec api anchore-cli image add openjdk:11.0.6-jre-slim

#get status
docker-compose exec api anchore-cli image list

#Show vulnerabilities
docker-compose exec api anchore-cli image vuln openjdk:11.0.6-jre-slim all
This gives you a list of vulnerabilities of the image you indicated you wanted scanned.

If you want to scan multiple images, for example to determine the most secure JRE 11.0.6 image, you can do the following in a Bash script:
 strings=(  
openjdk:11.0.6-jre-buster
openjdk:11.0.6-jre
openjdk:11.0.6-jre-slim-buster
openjdk:11.0.6-jre-slim
openjdk:11.0.6-jre-stretch
adoptopenjdk:11.0.6_10-jre-openj9-0.18.1
adoptopenjdk:11.0.6_10-jre-hotspot
adoptopenjdk:11.0.6_10-jre-openj9-0.18.1-bionic
adoptopenjdk:11.0.6_10-jre-hotspot-bionic
adoptopenjdk/openjdk11:jre-11.0.6_10-ubuntu
adoptopenjdk/openjdk11:jre-11.0.6_10
adoptopenjdk/openjdk11:jre-11.0.6_10-ubi-minimal
adoptopenjdk/openjdk11:jre-11.0.6_10-ubi
adoptopenjdk/openjdk11:jre-11.0.6_10-debianslim
adoptopenjdk/openjdk11:jre-11.0.6_10-debian
adoptopenjdk/openjdk11:jre-11.0.6_10-centos
adoptopenjdk/openjdk11:jre-11.0.6_10-alpine
mcr.microsoft.com/java/jre:11u6-zulu-alpine
mcr.microsoft.com/java/jre:11u6-zulu-centos
mcr.microsoft.com/java/jre:11u6-zulu-debian8
mcr.microsoft.com/java/jre:11u6-zulu-debian9
mcr.microsoft.com/java/jre:11u6-zulu-debian10
mcr.microsoft.com/java/jre:11u6-zulu-ubuntu
azul/zulu-openjdk-alpine:11.0.6-jre
)

for i in "${strings[@]}"; do
docker-compose exec api anchore-cli image add "$i"
done

Processing results

Now you have to wait a while for all the images to be scanned. If it's done, you can process the data.
 strings=(  
openjdk:11.0.6-jre-buster
openjdk:11.0.6-jre
openjdk:11.0.6-jre-slim-buster
openjdk:11.0.6-jre-slim
openjdk:11.0.6-jre-stretch
adoptopenjdk:11.0.6_10-jre-openj9-0.18.1
adoptopenjdk:11.0.6_10-jre-hotspot
adoptopenjdk:11.0.6_10-jre-openj9-0.18.1-bionic
adoptopenjdk:11.0.6_10-jre-hotspot-bionic
adoptopenjdk/openjdk11:jre-11.0.6_10-ubuntu
adoptopenjdk/openjdk11:jre-11.0.6_10
adoptopenjdk/openjdk11:jre-11.0.6_10-ubi-minimal
adoptopenjdk/openjdk11:jre-11.0.6_10-ubi
adoptopenjdk/openjdk11:jre-11.0.6_10-debianslim
adoptopenjdk/openjdk11:jre-11.0.6_10-debian
adoptopenjdk/openjdk11:jre-11.0.6_10-centos
adoptopenjdk/openjdk11:jre-11.0.6_10-alpine
mcr.microsoft.com/java/jre:11u6-zulu-alpine
mcr.microsoft.com/java/jre:11u6-zulu-centos
mcr.microsoft.com/java/jre:11u6-zulu-debian8
mcr.microsoft.com/java/jre:11u6-zulu-debian9
mcr.microsoft.com/java/jre:11u6-zulu-debian10
mcr.microsoft.com/java/jre:11u6-zulu-ubuntu
azul/zulu-openjdk-alpine:11.0.6-jre
)

echo Unknown,Critical,High,Medium,Low,Negligible,Image
for i in "${strings[@]}"; do
docker-compose exec api anchore-cli image vuln "$i" all | awk 'NR>1{print $3}' | sort -n | uniq -c >parse.txt
UNKNOWN=`cat parse.txt | grep Unknown | awk '{print $1}'`
CRITICAL=`cat parse.txt | grep Critical | awk '{print $1}'`
LOW=`cat parse.txt | grep Low | awk '{print $1}'`
MEDIUM=`cat parse.txt | grep Medium | awk '{print $1}'`
HIGH=`cat parse.txt | grep High | awk '{print $1}'`
NEG=`cat parse.txt | grep Negligible | awk '{print $1}'`
if [ -z "$UNKNOWN" ]; then
UNKNOWN=0
fi
if [ -z "$CRITICAL" ]; then
CRITICAL=0
fi
if [ -z "$LOW" ]; then
LOW=0
fi
if [ -z "$MEDIUM" ]; then
MEDIUM=0
fi
if [ -z "$HIGH" ]; then
HIGH=0
fi
if [ -z "$NEG" ]; then
NEG=0
fi
echo $UNKNOWN,$CRITICAL,$HIGH,$MEDIUM,$LOW,$NEG,"$i"
done
This provides a nice comma separated list which you can use in your favorite spreadsheet for some visualization

Next, you can draw some conclusions like
  • Newer OS versions are more secure
  • Alpine does better than Debian/Ubuntu. Debian/Ubuntu does better than RHEL/CentOS
  • Slim versions do slightly better than not so slim versions
  • No OpenJDK JRE 11.0.6 images scanned have critical vulnerabilities. Very few have high severity issues
  • In the OpenJDK images, when a new version is released, the underlying libraries and tools are also updated, reducing the number of vulnerabilities in newer versions.

Finally

I was surprised in how little time I could perform a vulnerability scan of a list of images. The integration options Anchore Engine provides, also seem powerful (Jenkins plugin, Kubernetes Admission Controller), although I did not try them out yet. There seems little reason not to cooperate a scan like this in your CI/CD environment. I suggest you give it a try!

This is just an example of a security related challenge. The container platform itself runs on an OS which you should check. Kubernetes and its native components can have vulnerabilities. Of course you should also keep an eye on already deployed images, since new vulnerabilities can be found. You should perform scans on your source code dependencies (see for example the OWASP dependency check here) and on the code itself (see here). Also outside of your source code it is advisable to do some security related integration tests, such as try out various XML based attacks when for example exposing SOAP services. Many challenges, but this one seems relatively easily tackled!

Production ready Kubernetes on your laptop. Kubespray on KVM

$
0
0
There are various options to install a production-like Kubernetes distribution on your laptop. Previously I tried out using the Canonical stack(Juju, MAAS, Charmed Kubernetes) for this. This worked nicely but it gave me the feeling that it was a bit Canonical specific and with the recent discussions around Snaps and the Canonical Snap Store, I decided to take a look at another way to install Kubernetes on my laptop in such a way that it would approximate a production environment. Of course first I needed to get my virtual infrastructure ready (KVM hosts) before I could use Kubespray to deploy Kubernetes. My main inspirations for this were two blog posts hereand here. Like with Charmed Kubernetes, the installed distribution is bare. It does not contain things like a private registry, distributed storage (read here) or load balancer (read here). You can find my scripts here (which are suitable for Ubuntu 20.04).

Provisioning infrastructure

This time I'm not depending one external tools such as Vagrant or MAAS to provide me with machines but I'm doing it 'manually' with some simple scripts. The idea is relatively simple. Use virt-install to create KVM VMs and install them using a Kickstart script which creates an ansible user and registers a public key so you can login using that user. Kubespray can then login and use ansible to install Kubernetes inside the VMs. 

As indicated before, I mainly used the scripts provided and described here but created my own versions to fix some challenges I encountered. The scripts are thin wrappers around KVM related commands so not much to worry about in terms of maintenance. You can execute the virt-install command multiple times to create more hosts.

What did I change in the scripts I used as base?

  • I used Ubuntu 20.04 as a base OS instead of 18.04 on which the scripts and blog post was based. Specifying the ISO file in the virt-install command in the create-vm script did not work for me. I decided to install by specifying a remote URL which did the trick: 'http://archive.ubuntu.com/ubuntu/dists/focal/main/installer-amd64/'.
  • The Kickstart file contained a public key for which I did not have the private key and an encrypted password of which I did not have an unencrypted version so I inserted my own public key (of course generated specifically for this purpose) and encrypted my own password.
  • It appeared virt-manager (KVM/QEMU GUI) and the virt-install command uses LIBVIRT_DEFAULT_URI="qemu:///system" while virsh commands use "qemu:///session". This caused some of the scripts to fail and VMs not to be visible. I added setting the parameter to qemu:///system in the scripts to avoid this.
  • I've added some additional thin wrapper scripts (like call_create_vm.sh, call_delete_vm.sh) to start the machines, create multiple machines with a single command and remove them again. Just to make life a little bit easier.

Creating KVM machines

First install required packages on the host. The below commands work on Ubuntu 18.04 and 20.04. Other OSs require different commands/packages to install KVM/QEMU and some other related things.

Install some packages
 sudo apt-get update  
sudo apt-get -y install bridge-utils qemu-kvm qemu virt-manager net-tools openssh-server mlocate libvirt-clients libvirt-daemon libvirt-daemon-driver-storage-zfs python-libvirt python3-libvirt system-config-kickstart virt-manager virtinst ansible

Clone the scripts

 git clone https://github.com/MaartenSmeets/k8s-prov.git  
cd k8s-prov

Create a public and private key pair

 ssh-keygen -t rsa -C ansible@host -f id_rsa

Create an encrypted password for the user ansible

 python encrypt-pw.py

Update ubuntu.ks

Update the ubuntu.ks file with the encrypted password and the generated public key. The Kickstart file already contains a public key for which the private key is provided and an encrypted password of which the plaintext is Welcome01. As indicated, these are for example purposes. Do not use them for production!

Start creating VMs!

Evaluate call_create_vm.sh for the number of VMs to create and resources per VM. By default it creates 4 VMs with each 2 cores assigned and 4Gb of memory. Next execute it.
 call_create_vm.sh

Installing Kubernetes using Kubespray

Now your infrastructure is ready but how to get Kubernetes on it? Kubespray is a composition of Ansible playbooks, inventory, provisioning tools, and domain knowledge for generic OS/Kubernetes clusters configuration management tasks. Kubespray can be run from various Linux distributions and allows installing Kubernetes on various other distributions. Kubespray comes with Terraform scripts for various cloud environments should you want to use those instead of providing your own KVM infra. Kubespray has quite a lot of Github stars, contributors and has been around for quite a while. It is part of the CNCF (here). I've also seen large customers using it to deploy and maintain their Kubernetes environment.

In order to use Kubespray, you need a couple of things such as some Python packages, a way to access your infrastructure and Ansible but (of course by sheer coincidence), you already fixed that in the previous step.

Clone the repository in a subdirectory of k8s-prov which you created earlier (so the commands can access the keys and scripts)
 git clone https://github.com/kubernetes-sigs/kubespray.git
cd kubespray
Install requirements
 pip install -r requirements.txt
Create an inventory
 rm -Rf inventory/mycluster/
cp -rfp inventory/sample inventory/mycluster
Use a script to obtain the KVM host IP addresses. These will be used to generate a hosts.yml file indicating what should be installed where.
 declare -a IPS=($(for n in $(seq 1 4); do ../get-vm-ip.sh node$n; done))
echo ${IPS[@]}
CONFIG_FILE=inventory/mycluster/hosts.yml \
  python3 contrib/inventory_builder/inventory.py ${IPS[@]}
Make life easy by letting it generate an admin.conf which can be used as ~/.kube/config 
 echo '  vars:'>>  inventory/mycluster/hosts.yml
echo '    kubeconfig_localhost: true'>>  inventory/mycluster/hosts.yml
Execute Ansible to provision the machines using the previously generated key
 export ANSIBLE_REMOTE_USER=ansible
ansible-playbook -i inventory/mycluster/hosts.yml --become --become-user=root cluster.yml --private-key=../id_rsa
Create your config file so kubectl can do its thing
 cp -rip inventory/mycluster/artifacts/admin.conf ~/.kube/config
Install kubectl (for Kubernetes)
 sudo snap install kubectl --classic
The dashboard URL. First do kubectl proxy to be able to access it at localhost:8001
 http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#/login
Allow the kube-system:clusterrole-aggregation-controller to access the dashboard
 kubectl create clusterrolebinding dashboard-admin -n default --clusterrole=cluster-admin --serviceaccount=kube-system:clusterrole-aggregation-controller
Get a token to access the dashboard
 kubectl -n kube-system describe secrets `kubectl -n kube-system get secrets | awk '/clusterrole-aggregation-controller/ {print $1}'` | awk '/token:/ {print $2}'
Login and enjoy!



OpenEBS: cStor storage engine on KVM

$
0
0
OpenEBS provides a Kubernetes native distributed storage solution which is friendly on developers and administrators. It is completely open source and part of the CNCF. Previously I wrote about installing and using OpenEBS, Jiva storage engine, on the Charmed Kubernetes distribution of Canonical. The Jiva storage class uses storage inside managed pods. cStor however can use raw disks attached to Kubernetes nodes. Since I was trying out Kubespray (also a CNCF project) on KVM and it is relatively easy to attach raw storage to KVM nodes, I decided to give cStor a try. cStor (which uses ZFS behind the scenes) is also the more recent and more robust storage engine and suitable for more serious workloads. See here. You can download the scripts I used to setup my Kubernetes environment here.


Preparations

Prerequisites

I used the setup described here. Kickstart is used to create KVM VMs with the names node1, node2, etc. Kubespray is used to provision the nodes with Kubernetes. A prerequisite for the following steps is a running Kubernetes environment created using the described method.

Preparing the environment

In order to use OpenEBS, an iSCSI client needs to be installed on the nodes. This is described in the prerequisites of OpenEBS here. For my setup I created a small script to loop over my nodes and execute some SSH commands on them in order to do that (the exec_ssh.sh script here).

You can also manually execute the following commands on your node hosts after having logged in there, should your environment look differently. These commands are Ubuntu based and have been checked on 18.04 and 20.04. They will probably work on other Debian based distributions but for other OSs, check the previously mentioned OpenEBS documentation on how to install the client.

sudo apt-get -y install open-iscsi
sudo systemctl enable --now iscsid
sudo systemctl enable iscsid
sudo systemctl start iscsid

Preparing and attaching raw storage

In my environment, I had KVM machines named node1, node2, etc. In order to create raw storage I did the following:

#Go to the location where your images are stored. In my case this is /home/maarten/k8s/k8s-prov/machines/images
cd /home/maarten/k8s/k8s-prov/machines/images
#Create 4 times (one for every node)
for n in $(seq 1 4); do
    #Create a raw storage image of 10Gb
    qemu-img create -f raw node$n-10G 10G
    #Attach the storage to the node and call it vdb
    virsh attach-disk node$n /home/maarten/k8s/k8s-prov/machines/images/node$n-10G vdb --cache none
done

OpenEBS

Installing OpenEBS is quite easy. I've used 1.12.0. As described, a prerequisite is having a running Kubernetes environment and of course a working ~/.kube/config in order to use kubectl commands. Helm also needs to be installed.

Preparations

In order to install kubectl and helm on Ubuntu you can do:

sudo snap install kubectl --classic
sudo snap install helm --classic

In order to install OpenEBS you can do the following:

helm repo add openebs https://openebs.github.io/charts
helm repo update
kubectl create namespace openebs
helm install --namespace openebs openebs openebs/openebs

Configuring OpenEBS

Now OpenEBS needs to know which devices it is allowed to use. The following command updates the ConfigMap which specifies which devices to include. /dev/vdb should be included since it uses our newly created raw disk files.

kubectl get -n openebs cm openebs-ndm-config -o yaml |   sed -e 's|include: ""|include: "/dev/vdb"|' |   kubectl apply -f -

Next you can check if the raw devices are available

kubectl get blockdevice -n openebs

In my case this gives output like:

NAME                                           NODENAME   SIZE          CLAIMSTATE   STATUS   AGE
blockdevice-85b3dd88549b7bb2ca9aada391750240   node2      10737418240   Unclaimed    Active   12m
blockdevice-9955ca806fd32c2e18e5293f597653b5   node1      10737418240   Unclaimed    Active   12m
blockdevice-cb09bfc8ae80591f356fe3153446064e   node3      10737418240   Unclaimed    Active   12m
blockdevice-f4629d6ac8d0d9260ff8a552640f30cf   node4      10737418240   Unclaimed    Active   12m

Now you can create a cStor storage pool which uses these blockdevices. The following is an example since the names of the blockdevices are specific. You should update it to reflect your specific blockdevices.

kubectl apply -n openebs -f - <<END  
#Use the following YAMLs to create a cStor Storage Pool.
apiVersion: openebs.io/v1alpha1
kind: StoragePoolClaim
metadata:
  name: cstor-disk-pool
  annotations:
    cas.openebs.io/config: |
      - name: PoolResourceRequests
        value: |-
            memory: 2Gi
      - name: PoolResourceLimits
        value: |-
            memory: 4Gi
spec:
  name: cstor-disk-pool
  type: disk
  poolSpec:
    poolType: striped
  blockDevices:
    blockDeviceList:
    - blockdevice-85b3dd88549b7bb2ca9aada391750240
    - blockdevice-9955ca806fd32c2e18e5293f597653b5
    - blockdevice-cb09bfc8ae80591f356fe3153446064e
    - blockdevice-f4629d6ac8d0d9260ff8a552640f30cf
END

Now you can check if the blockdevices have correctly been claimed:

kubectl get csp

This will give output like:

NAME                   ALLOCATED   FREE    CAPACITY   STATUS    READONLY   TYPE      AGE
cstor-disk-pool-3csp   77K         9.94G   9.94G      Healthy   false      striped   3m9s
cstor-disk-pool-6cbb   270K        9.94G   9.94G      Healthy   false      striped   3m10s
cstor-disk-pool-jdn4   83K         9.94G   9.94G      Healthy   false      striped   3m10s
cstor-disk-pool-wz7x   83K         9.94G   9.94G      Healthy   false      striped   3m10s

Next you can create a storage class to use the newly created storage pool:

kubectl apply -n openebs -f - <<END  
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: openebs-sc-statefulset
  annotations:
    openebs.io/cas-type: cstor
    cas.openebs.io/config: |
      - name: StoragePoolClaim
        value: "cstor-disk-pool"
      - name: ReplicaCount
        value: "3"
provisioner: openebs.io/provisioner-iscsi
END

You can set it to be the default with the following command:

kubectl patch storageclass openebs-sc-statefulset -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

Using storage

Now that we have a storage class, we can try it out!

Create a persistent volume claim

kubectl create -n jenkins -f - <<END
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-pv-claim
spec:
  storageClassName: openebs-sc-statefulset
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
END

Configure and install Jenkins

cat << EOF > jenkins-config.yaml
persistence:
    enabled: true
    size: 5Gi
    accessMode: ReadWriteOnce
    existingClaim: jenkins-pv-claim
    storageClass: "openebs-sc-statefulset"
EOF

helm install my-jenkins-release -f jenkins-config.yaml stable/jenkins --namespace jenkins

Confirm it actually works

Now you can check under the Jenkins namespace everything comes up



After you have executed the helm installation of Jenkins, there is an instruction on how to login. This allows you to confirm Jenkins is actually running.

You can also see storage has actually been claimed and that, as specified in the ReplicaCount of the StorageClass, the data is distributed over 3 replica's.

kubectl get csp
NAME                   ALLOCATED   FREE    CAPACITY   STATUS    READONLY   TYPE      AGE
cstor-disk-pool-3csp   221M        9.72G   9.94G      Healthy   false      striped   36m
cstor-disk-pool-6cbb   178K        9.94G   9.94G      Healthy   false      striped   36m
cstor-disk-pool-jdn4   221M        9.72G   9.94G      Healthy   false      striped   36m
cstor-disk-pool-wz7x   221M        9.72G   9.94G      Healthy   false      striped   36m

Kubernetes: Building and deploying a Java service with Jenkins

$
0
0

Kubernetes has become the de facto container orchestration platform to run applications on. Java applications are no exception to this. When using a PaaS provider to give you a hosted Kubernetes, sometimes that provider also provides a CI/CD solution. However this is not always the case. When hosting Kubernetes yourself, you also need to implement a CI/CD solution.

Jenkins is a popular tool to use when implementing CI/CD solutions. Jenkins can also run quite easily in a Kubernetes environment. When you have Jenkins installed, you need to have a Git repository to deploy your code from, a Jenkins pipeline definition, tools to wrap your Java application in a container, a container registry to deploy your container to and some files to describe how the container should be deployed and run on Kubernetes. In this blog post I'll describe a simple end-to-end solution to deploy a Java service to Kubernetes. This is a minimal example so there is much room for improvement. It is meant to get you started quickly.

The Java application

I've created a simple Spring Boot service. You can find the code here. I hosted it on GitHub since it was easy to use as source for Jenkins.

I needed something to wrap my Java application inside a container. There are various plug-ins available like for example the Spotify dockerfile-maven plug-in (here) and the fabric8 docker-maven-plugin (here). They both require access to a Docker daemon though. This can be complicated, especially when running Jenkins slaves within Kubernetes. There are workarounds but I did not find any that seemed both easy and secure. I decided to go for Google's Jib to build my containers since it didn't have that requirement. 

Docker build flow:


Jib build flow:

The benefits of reducing dependencies for the build process are obvious. In addition Jib also does some smart things splitting the Java application in different container layers. See here. This reduces the amount of storage required for building and deploying new versions as often some of the layers, such as the dependencies layer, don't change and can be cached. This can also reduce build time. As you can see, Jib does not use a Dockerfile so the logic usually in the Dockerfile can be found in the plugin configuration inside the pom.xml file. Since I did not have a private registry available at the time of writing, I decided to use DockerHub for this. You can find the configuration for using DockerHub inside the pom.xml. It uses environment variables set by the Jenkins build for the credentials (and only in the Jenkins slave which is created and after the build destroyed). This seemed more secure than passing them in the Maven command-line.

Note that Spring buildpacks could provide similar functionality. I have not looked into them yet though. 

Installing Jenkins

For my Kubernetes environment I have used the setup described here. You also need a persistent storage solution as prerequisite for Jenkins. In a SaaS environment, this is usually provided, but if it is not or you are running your own installation, you can consider using OpenEBS. How to install OpenEBS is described here. kubectl (+ Kubernetes configuration .kube/config) and helm need to be installed on the machine from which you are going to perform the Jenkins deployment.

After you have and a storage class, you can continue with the installation of Jenkins.

First create a PersistentVolumeClaim to store the Jenkins master persistent data. Again, this is based on the storage class solution described above.

 kubectl create ns jenkins  

kubectl create -n jenkins -f - <<END
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-pv-claim
spec:
storageClassName: openebs-sc-statefulset
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
END

Next install Jenkins. Mind that recently the Jenkins repository for the most up to date Helm charts has moved.

 cat << EOF > jenkins-config.yaml  
persistence:
enabled: true
size: 5Gi
accessMode: ReadWriteOnce
existingClaim: jenkins-pv-claim
storageClass: "openebs-sc-statefulset"
EOF

helm repo add jenkinsci https://charts.jenkins.io
helm install my-jenkins-release -f jenkins-config.yaml jenkinsci/jenkins --namespace jenkins

Now you will get a message like:

Get your 'admin' user password by running:

 printf $(kubectl get secret --namespace jenkins my-jenkins-release -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

Get the Jenkins URL to visit by running these commands in the same shell and login!

 export POD_NAME=$(kubectl get pods --namespace jenkins -l "app.kubernetes.io/component=jenkins-master" -l "app.kubernetes.io/instance=my-jenkins-release" -o jsonpath="{.items[0].metadata.name}")
 kubectl --namespace jenkins port-forward $POD_NAME 8080:8080

Now visit http://localhost:8080 in your browser

And login with admin using the previously obtained password

Configuring Jenkins

The pipeline

Since I'm doing 'configuration as code' I created a declarative Jenkins pipeline and also put it in GitHub next to the service I wanted to deploy. You can find it here. As you can see, the pipeline has several dependencies.
  • presence of tool configuration in Jenkins
    • Maven
    • JDK
  • the Kubernetes CLI plugin (withKubeConfig)
    This plugin makes Kubernetes configuration available within the Jenkins slaves during the build process
  • the Pipeline Maven Integration plugin (withMaven)
    This plugin archives Maven artifacts created such as test reports and JAR files

Tool configuration

JDK

The default JDK plugin can only download old Java versions from Oracle. JDK 11 for example is not available this way. I added a new JDK as followed:

I specified a download location of the JDK. There are various available such as AdoptOpenJDK or the one available from Red Hat or Azul Systems. Inside the archive I checked in which subdirectory the JDK was put. I specified this subdirectory in the tool configuration.


Please note that downloading the JDK during each build can be slow and prone to errors (suppose the download URL changes). A better way is to make it available as a mount inside the Jenkins slave container. For this minimal setup I didn't do that though.

You also need to define a JAVA_HOME variable pointing to a location like indicated below. Why? Well, you also want Maven to use the same JDK.


Maven

Making the Maven tool available is easy luckily.


The name of the Maven installation is referenced in the Jenkins pipeline like:
  tools {  
jdk 'jdk-11'
maven 'mvn-3.6.3'
}

stages {
stage('Build') {
steps {
withMaven(maven : 'mvn-3.6.3') {
sh "mvn package"
}
}
}
Kubectl

For kubectl there is no tool definition in the Jenkins configuration available so I did the following:
 sh 'curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"'
sh 'chmod u+x ./kubectl'
sh './kubectl apply -f k8s.yaml'
As you can see, a k8s.yaml file is required. 

You can generate it as follows. First install a loadbalancer. Mind the IPs; they are specific for my environment. You might need to provide your own. Then create a deployment and service. I've added the Ingress myself. The complete file can be found here.
 kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml
# On first install only
kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"

kubectl apply -f - <<END
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.122.150-192.168.122.255
END
 kubectl create deployment spring-boot-demo --image=docker.io/maartensmeets/spring-boot-demo --dry-run=client -o=yaml > k8s.yaml
echo --- >> k8s.yaml
kubectl create service loadbalancer spring-boot-demo --tcp=8080:8080 --dry-run=client -o=yaml >> deployment.yaml

Credential configuration

Kubernetes

In order for Jenkins to deploy to Kubernetes, Jenkins needs credentials. An easy way to achieve this is by storing a config file (named 'config') in Jenkins. This file is usually used by kubectl and found in .kube/config. It allows Jenkins to apply yaml configuration to a Kubernetes instance.


The file can then be referenced from a Jenkins pipeline with the Kubernetes CLI plugin like in the snipped below.
 withKubeConfig([credentialsId: 'kubernetes-config']) {  
sh 'curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"'
sh 'chmod u+x ./kubectl'
sh './kubectl apply -f k8s.yaml'
}

DockerHub

I used DockerHub as my container registry. The pom.xml file references the environment variables DOCKER_USERNAME and DOCKER_PASSWORD but how do we set them from the Jenkins configuration? By storing them as credentials of course! 


In the pipeline you can access them as followed:
 withCredentials([usernamePassword(credentialsId: 'docker-credentials', usernameVariable: 'DOCKER_USERNAME', passwordVariable: 'DOCKER_PASSWORD')]) {  
sh "mvn jib:build"
}

This sample stores credentials directly in Jenkins. You can also use the Jenkins Kubernetes Credentials Provider to store credentials in Kubernetes as secrets. This provides some benefits in management of the credentials, for example with kubectl it is easy to script changes. A challenge is giving the Jenkins user sufficient but not too much privileges on Kubernetes.

GitHub

In order to access GitHub, also some credentials are required:


Jenkins job configuration

The configuration of the Jenkins job is actually almost the least exciting. The pipeline is defined outside of Jenkins. The only thing Jenkins needs to know is where to find the sources and the pipeline.


Create a Multibranch Pipeline job. Multibranch is quite powerful since it allows you to build multiple branches with the same job configuration.

Open the job configuration and specify the Git source. 


The build is based on the Jenkinsfile which contains the pipeline definition.


After you have saved the job it will start building immediately. 

Building and running

Webhook configuration

What is lacking here is Webhook configuration. See for example here. This will cause Jenkins builds to be triggered when branches are created, pull requests are merged, commits happen, etc. Since I'm running Kubernetes locally I do not have a publicly exposed endpoint as target for the GitHub webhook. You can use a simple service like smee.io to get a public URL and forward it to your local Jenkins. Added benefit is that it generally does not care about things like firewalls (similar to ngrok but Webhook specific and doesn't require an account).


After you have installed the smee CLI and have the Jenkins port forward running (the thing which makes Jenkins available on port 8080) you can do (of course your URL will differ):

  smee -u https://smee.io/z8AyLYwJaUBBDA5V -t http://localhost:8080/github-webhook/

This starts the Webhook proxy and forwards requests to the Jenkins webhook URL. In GitHub you can add a Webhook to call the created proxy and make it trigger Jenkins builds.


Next you can confirm it works from Smee and from GitHub



If I now create a new branch in GitHub


It will appear in Jenkins and start building



If you prefer a nice web-interface, I can recommend Blue Ocean for Jenkins. It can easily be installed by installing the Blue Ocean plugin.

Finally

After you've done all the above, you can access your service running in your Kubernetes environment after a GitHub commit which fires off a webhook which is forwarded by Smee to Jenkins which triggers a Multibranch Pipeline build which builds the Java service, wraps it in a container using Jib and deploys the container to Kubernetes, provides a deployment and a service. The service can be accessed via the MetalLB load-balancer.



Surprisingly easy: Container vulnerability scanning in a Jenkins pipeline running on Kubernetes using Anchore Engine

$
0
0

Anchore Engine is a popular open source tool for container image inspection and vulnerability scanning. It is easily integrated in a Kubernetes environment as an admission controller or in a Jenkins build pipeline using a plugin. A while ago I took a look at Anchore Engine and created a small introductory presentation and Katacoda scenario for it. The Katacoda scenario allows you to try out Anchore Engine without having to setup your own container environment. In this blog I'll go a step further and illustrate how you can incorporate an Anchore Engine container scan inside your Java build pipeline which I illustrated here. Anchore Engine is deployed to Kubernetes, configured in Jenkins (which also runs on Kubernetes) and incorporated in a Jenkins Pipeline during a build process. Only if the container has been deemed secure by the configured Anchore Engine policy, is it allowed to be deployed to Kubernetes. I will also show how to update policies using the CLI.

Prerequisites

In order for the following steps to work, you need to have a Kubernetes cluster running and kubectl + helm configured to allow deployments to it. The Kubernetes cluster is expected to have internet access in order to download the container image from DockerHub and to update the Anchore Engine vulnerability lists. I've used the following Kubernetes environment using the following storage configuration and the following Jenkins installation for this.

Deploying Anchore Engine to Kubernetes

The following can be used to deploy Anchore Engine to Kubernetes. This is based on the following documentation. The Helm chart installs a PostgreSQL database, but you can also use one supplied externally. In production you should definitely use your own PostgreSQL installation which has been deployed in a high-available setup. Of course I would also change Welcome01 to a more secure one if you need it.

 cat << EOF > anchore_values.yaml  
postgresql:
postgresPassword: Welcome01
persistence:
size: 10Gi

anchoreGlobal:
defaultAdminPassword: Welcome01
defaultAdminEmail: maarten.smeets@amis.nl
EOF

helm repo add anchore https://charts.anchore.io
helm repo update

kubectl create ns anchore
helm install anchore-release -n anchore -f anchore_values.yaml anchore/anchore-engine

In order to check the deployment, you are provided with several handy commands after the installation is finished such as a command to start a container which has the anchore-cli installed. You also get information on the URL at which you can access Anchore Engine from within the Kubernetes cluster. You need this information for the Jenkins configuration.

 kubectl run -i --tty anchore-cli --restart=Always --image anchore/engine-cli --env ANCHORE_CLI_USER=admin --env ANCHORE_CLI_PASS=Welcome01 --env ANCHORE_CLI_URL=http://anchore-release-anchore-engine-api.anchore.svc.cluster.local:8228/v1/    

[anchore@anchore-cli anchore-cli]$ anchore-cli system status
Service simplequeue (anchore-release-anchore-engine-simplequeue-75b49c55c5-bktbv, http://anchore-release-anchore-engine-simplequeue:8083): up
Service policy_engine (anchore-release-anchore-engine-policy-5576ff74b4-8rv5r, http://anchore-release-anchore-engine-policy:8087): up
Service apiext (anchore-release-anchore-engine-api-5b5ddb8cd6-k66hm, http://anchore-release-anchore-engine-api:8228): up
Service catalog (anchore-release-anchore-engine-catalog-5648d4df64-5rtq9, http://anchore-release-anchore-engine-catalog:8082): up
Service analyzer (anchore-release-anchore-engine-analyzer-5588cc6964-489h7, http://anchore-release-anchore-engine-analyzer:8084): up

Engine DB Version: 0.0.13
Engine Code Version: 0.8.1

Configuring Jenkins

The Jenkins configuration is relatively straightforward. You need to install the Anchore Container Image Scanner plugin:


Next you need to configure the Anchore Engine location and credentials. The location is the URL as previously obtained from the output of the Helm chart installation and the credentials as supplied in the anchore_values.yaml file.


Configure and run your pipeline

I created a minimal Jenkins Java build pipeline a while back which I wanted to expand with Anchore Engine scanning. I added the following to my pipeline:

   stage('Anchore analyse') {  
steps {
writeFile file: 'anchore_images', text: 'docker.io/maartensmeets/spring-boot-demo'
anchore name: 'anchore_images'
}
}

Next I ran it:

It can take a while before Anchore Engine has scanned the image. After it is completed, you can see the results in Jenkins.


Updating the policy using the CLI

I get a warning about a Dockerfile which I did not supply. This is correct since I created my container using Google Jib which does not require a Dockerfile or Docker daemon installation to build and push images. I don want to see this warning though because it is irrelevant to my build process. Based on the following, you can change the 'gate action' when this 'vulnerability' is found using the Anchore CLI.

First enter the Anchore CLI container
kubectl run -i --tty anchore-cli --restart=Always --image anchore/engine-cli --env ANCHORE_CLI_USER=admin --env ANCHORE_CLI_PASS=Welcome01 --env ANCHORE_CLI_URL=http://anchore-release-anchore-engine-api.anchore.svc.cluster.local:8228/v1/    

Next determine and download the used policy

 [anchore@anchore-cli anchore-cli]$ anchore-cli policy list  
Policy ID Active Created Updated
2c53a13c-1765-11e8-82ef-23527761d060 True 2020-10-04T11:30:19Z 2020-10-04T11:30:19Z
[anchore@anchore-cli anchore-cli]$ anchore-cli policy get 2c53a13c-1765-11e8-82ef-23527761d060 --detail > /tmp/policybundle.json

When you edit the file with vi, you can find the dockerfile warning. You can also see at the bottom of the file a whitelists definition. In this whitelist we can add the following in the items tag to exclude this specific warning.


The gate and trigger_id can be determined from the report given by the Anchore plugin in Jenkins or by looking up the vulnerability in the json file you just downloaded.

Next update the policy and activate it:

 [anchore@anchore-cli anchore-cli]$ anchore-cli policy add /tmp/policybundle.json   
Policy ID: 2c53a13c-1765-11e8-82ef-23527761d060
Active: False
Source: local
Created: 2020-10-04T11:30:19Z
Updated: 2020-10-04T14:03:25Z

[anchore@anchore-cli anchore-cli]$ anchore-cli policy activate 2c53a13c-1765-11e8-82ef-23527761d060
Success: 2c53a13c-1765-11e8-82ef-23527761d060 activated

When you now execute the pipeline again, you will notice the warning is gone (listed as Go) and it is indicated as whitelisted.


Finally

Deploying Anchore Engine to your Kubernetes cluster is relatively easy. Using it in a Jenkins pipeline is also. By adding it to your build process, you can confirm images used do not have vulnerabilities you might want to avoid. Also using the Anchore CLI you can edit the policy which determines if the build succeeds or not. 

Anchore Engine also provides a notification system. If you scan a container and new vulnerabilities are discovered because vulnerability feeds get updated, you can be informed by webhook calls. This allows you to take action when needed or feed dashboards (like an ELK stack).

What this does not provide is in-debt scanning of images, just the libraries which are present in the container. It depends on a publicly available vulnerability database. Anchore has an Enterprise version available which adds additional vulnerability feeds. It also provides an option to create a local proxy for the vulnerability feeds which you are not dependent on an internet connection. Also you get reports on entire Docker registries and a GUI for configuring policies. The Enterprise version has advanced SSO/RBAC options and an easy way to integrate notifications with Slack, Jira, GitHub, etc. If you want to use Anchore Engine at scale, I would take a look at the Enterprise version.

If you want to scan for example Java library dependencies, you can use the OWASP dependency check (of which (amongst others) a Maven plugin and Gradle plugin are available). By using several measures to scan your application/container/image for vulnerabilities, you can make sure your application is safe to run! In this blog post I tried to show that implementing Anchore Engine (in a basic setup) is a piece of cake and there is no good reason not to use it.

Jenkins Pipeline: SonarQube and OWASP Dependency-Check

$
0
0

The OWASP top 10 has listed the following vulnerability for several years (at least in 2013 and 2017): using components with known vulnerabilities. But software nowadays can be quite complex consisting of many dependencies. How do you know the components and versions of those components do not contain known vulnerabilities? Luckily the OWASP foundation has also provided a dependency-check tool with plugins for various languages to make detecting this more easy. In this blog post I'll show how you can incorporate this in a Jenkins pipeline running on Kubernetes and using Jenkens and SonarQube to display the results of the scan.


Prerequisites

I used the environment described here. This includes a ready configured kubectl and helm installation. For the Jenkins installation and basic pipeline configuration I used the following here. In order to execute the below steps you should have at least a Kubernetes and Jenkins installation ready. If you want to use the literal code samples, you also require the Jenkins configuration as described including this.

OWASP dependency-check

The OWASP foundation provided Dependency-Check plugins for various build tools such as Ant, Gradle and Maven and a Jenkins plugin. They also have a standalone CLI tool available. Mind that the more specific a plugin you use, the more relevant the findings will be. You can for example use the Dependency-Check Jenkins plugin to perform a scan, but it will not understand how dependencies inside a pom.xml work so will not give sufficiently useful results. You will get something like below:

When you implement the Maven Dependency-Check plugin to produce results and the Jenkins Dependency-Check plugin to get those results visible in Jenkins, you get results which are specific to a Maven build of your Java application. This is quite useful! When using this you will get more accurate results like below.


The Jenkins Dependency-Check plugin (which can be used within a pipeline) also produces trend graphs and html reports inside Jenkins.


Thus use the Maven Dependency-Check plugin to scan your project and use the Jenkins plugin to publish the results generated from the scan to Jenkins. After you have installed and configured SonarQube, you can use the same results to publish them to SonarQube.

Maven plugin configuration

You can find the pom.xml file I used here. You can execute the scan by running mvn dependency-check:check.

<plugin>  
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>6.0.2</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<!-- Generate all report formats -->
<format>ALL</format>
<!-- Don't use Nexus Analyzer -->
<centralAnalyzerEnabled>false</centralAnalyzerEnabled>
<!-- Am I the latest version? -->
<versionCheckEnabled>true</versionCheckEnabled>
</configuration>
</plugin>

Notice the format ALL is specified. This generates an HTML, JSON, XML and CSV report in the target folder. SonarQube will use the JSON report and Jenkins the XML report. I have not looked at the central analyzer (this appears to be a feature which is part of the commercial Nexus distribution). This might help reduce build time since the vulnerability files can be shared across scans and do not need to be downloaded every time.

You can browse the HTML report yourself if you like. Using this you can confirm that the dependencies were identified and actually scanned. My scan found 0 vulnerabilities so I was worried the scan was not executed correctly but the results showed the different dependencies and how they were evaluated to they were correct. A new Spring Boot version does not contain vulnerable dependencies as it should!

Installing SonarQube

There are various ways you can install SonarQube on Kubernetes. There are Docker images available which require you to create your own Kubernetes resources, but also a Helm chart. The Helm chart is really easy to use. For example, it creates a PostgreSQL database for the SonarQube installation for you without additional effort. The PostgreSQL database which is created is of course not highly available, clustered, etc. Using Helm value overrides, you can specify your own PostgreSQL DB to use which you can setup to fit your availability needs. The default username and password is admin. Of course change this in a production environment. A small drawback is that the chart is not 100% up to date. Currently it installs version 8.3 while 8.5 is already available. I'm not sure if this is an indication not much maintenance is being done on the chart. I hope this is not the case of course, else I would not recommend it. If you do use it, keep this in mind. For a lab scenario like this one I do not care whether I get version 8.3 or 8.5.

Installing SonarQube on Kubernetes can be done with the following commands using the helm chart;

 helm repo add oteemocharts https://oteemo.github.io/charts  
helm repo update
kubectl create ns sonar
helm install -n sonar sonar-release oteemocharts/sonarqube

After the installation is complete, you can access it with:

 export POD_NAME=$(kubectl get pods --namespace sonar -l "app=sonarqube,release=sonar-release" -o jsonpath="{.items[0].metadata.name}")  
kubectl -n sonar port-forward $POD_NAME 8080:9000

And going to http://localhost:8080. Username and password are as indicated admin


Configuring SonarQube in Jenkins

In the previously described setup, you can access Jenkins with:

 export POD_NAME=$(kubectl get pods --namespace jenkins -l "app.kubernetes.io/component=jenkins-master" -l "app.kubernetes.io/instance=my-jenkins-release" -o jsonpath="{.items[0].metadata.name}")  
printf $(kubectl get secret --namespace jenkins my-jenkins-release -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo
kubectl --namespace jenkins port-forward $POD_NAME 8081:8080

The second line gives the password of the admin user you can use to login. The last command makes a Jenkins pod available on localhost port 8081. 

In SonarQube you can obtain a secret. The SonarQube pipeline plugin in Jenkins can be configured to use the secret to store results from the build/dependency-check in SonarQube.





Important to mind is that you should not configure the SonarQube Scanner in Jenkins. This is a generic scanner when there is no specific scanner for your build available. Since we are using a Maven build and there is a Maven scanner as plugin available, it is preferable to use that one. A similar logic holds for the dependency-check; preferably use one inside your build over a generic scanner.

Jenkins pipeline

I used the following stages in my pipeline to perform the scan and load the results in Jenkins and SonarQube.
   stage ('OWASP Dependency-Check Vulnerabilities') {  
steps {
withMaven(maven : 'mvn-3.6.3') {
sh 'mvn dependency-check:check'
}

dependencyCheckPublisher pattern: 'target/dependency-check-report.xml'
}
}

stage('SonarQube analysis') {
steps {
withSonarQubeEnv(credentialsId: 'sonarqube-secret', installationName: 'sonarqube-server') {
withMaven(maven : 'mvn-3.6.3') {
sh 'mvn sonar:sonar -Dsonar.dependencyCheck.jsonReportPath=target/dependency-check-report.json -Dsonar.dependencyCheck.xmlReportPath=target/dependency-check-report.xml -Dsonar.dependencyCheck.htmlReportPath=target/dependency-check-report.html'
}
}
}
}
As you can see they have several dependencies. They depend on the OWASP Dependency-Check plugin for publishing results and the SonarQube Scanner for Jenkins to set environment variables which contain the required credentials/secret/hostname to access SonarQube. I'm also using withMaven from the Pipeline Maven Integration plugin. As you can see, I'm using the Maven build to perform the scan and not the dependencyCheck from the Jenkins plugin. To process the results I'm using the SonarQube plugin from Maven instead of the SonarQube Scanner from Jenkins.

A challenge which I didn't manage to solve on short notice is that Jenkins creates a temporary build container in which it executes the pipeline. After the build, the container will be disposed of. During the build process the OWASP Dependency-Check downloads vulnerability data. This takes a while (around 7 minutes on my machine). 


During a next build, since the build container had been destroyed already, it has to download all the vulnerability data again. The data needs to be stored in a persistent way so only an update of vulnerability data is required which saves a lot of time. Maybe mount a prepared tools container during the build or use an external analyzer (as indicated Sonatype Nexus provides one in their commercial version).

Executing the pipeline

When you execute the pipeline, SonarQube gets fed with various results and can also produce a measure of technical debt in your project. In this case test coverage (produced by the Maven Jacoco plugin) and data produced by the OWASP Dependency-Check. 



SonarQube makes a verdict on whether the build passes or not and this is displayed in Jenkins by the SonarQube Scanner plugin.

You can of course further expand this to include more thorough code quality checks by including Maven plugins such as Checkstyle, PMD and JDepend.

Python: A Google Translate service using Playwright

$
0
0

There are a lot of use-cases in which you might want to automate a web-browser. For example to automate tedious repetitive tasks or to perform automated tests of front-end applications. There are also several tools available to do this such as Selenium, Cypress and Puppeteer. Several blog posts and presentations by Lucas Jellema picked my interest in Playwright so I decided to give it a try. I'm not a great fan of JavaScript so I decided to go with Python for this one. I also did some tests with wrk, a simple yet powerful HTTP bench-marking tool, to get an indication about how Playwright would handle concurrency and was not disappointed.


Introduction

Playwright

The following here gives a nice comparison of Selenium, Cypress, Puppeteer and Playwright. Microsoft Playwright has been created by the same people who created Google Puppeteer and is relatively new. Playwright communicates bidirectionally with the browser. This allows events in the browser to trigger events in your scripts (see here). This is something which can be done but is more difficult using something like Selenium (see here, I suspect polling is usually involved). With Selenium you also often need to build delays inside your scripts. Playwright provides extensive options for waiting for things to happen and since the interaction is bidirectional, I suspect polling will not be used which increases performance and makes scripts more robust and fast.

I used PlayWright on Python. The Node.js implementation is more mature. On Python, before the first official non-alpha release, there might still be some breaking changes in the API so the sample provided here might not work in the future due to those API changes. Since the JavaScript and Python API are quite similar, I do not expect major changes though.

Python

Python 3.4 introduced the asyncio module and since Python 3.5 you can use keywords like async and await. Because I was using async libraries and I wanted to wrap my script in a webservice, I decided to take a look at what webservice frameworks are available for Python. I stumbled on this comparison. Based on the async capabilities, simple syntax, good performance and (claimed) popularity, I decided to go with Sanic.


Another reason for me to go with Python is that it is very popular in the AI/ML area. If in the future I want to scrape a lot of data from websites and do smart things with that data, I can stay within Python and do not have to mix several languages.

Google Translate API

Of course Google provides a commercial translate API. If you are thinking about seriously implementing a translate API, definitely go with that one since is is made to do translations and provide SLAs. I decided to create a little Python REST service to use the Google Translate website (a website scraper). For me this was a tryout of Playwright so in this example, I did not care about support, performance or SLAs. If you overuse the site, you will get Capcha's and I did not automate those away.

Getting things ready

I started out with a clean Ubuntu 20.04 environment. I tried JupyterLabs first but Playwright and JupyterLabs did not seem to play well together since probably JupyterLabs also extensively uses a browser itself. I decided to go with PyCharm. PyCharm has some code completion features which I like among other things and of course the interface is similar to IntelliJ and DataGrip which I also use for other things.

 #Python 3 was already installed. pip wasn't yet  
sudo apt-get install python3-pip
sudo pip3 install playwright
sudo pip3 install lxml
#Webservice framework
sudo pip3 install sanic
sudo apt-get install libenchant1c2a
#This installs the browsers which are used by Playwright
python3 -m playwright install
#PyCharm
sudo snap install pycharm-community --classic

The Google Translate API scraper


from playwright import async_playwright
from sanic import Sanic
from sanic import response

app = Sanic(name='Translate application')

@app.route("/translate")
async def doTranslate(request):
async with async_playwright() as p:
sl = request.args.get('sl')
tl = request.args.get('tl')
translate = request.args.get('translate')
browser = await p.chromium.launch() # headless=False
context = await browser.newContext()
page = await context.newPage()
await page.goto('https://translate.google.com/?sl='+sl+'&tl='+tl+'&op=translate')
textarea = await page.waitForSelector('//textarea')
await textarea.fill(translate)
waitforthis = await page.waitForSelector('div.Dwvecf',state='attached')
result = await page.querySelector('span.VIiyi >> ../span/span/span')
textresult = await result.textContent()
await browser.close()
return response.json({'translation':textresult})

if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)
You can also find the code here.

How does it work?

The app.run command at the end starts an HTTP server on port 5000. @app.route indicates the function doTranslate will be available at /translate. The async function receives a request. This request is used to obtain the GET arguments which are used to indicate 'target language' (tl), 'source language' (sl) and the text to translate (translate). Next a Chrome browser is started in headless mode. Headless is the default for Playwright but during development, it helps to disable this headless mode so you can see what happens in the browser. The Google translate site is opened with the source and target language as GET parameters. Next Playwright waits until a textarea appears (the p[age is fully loaded). It fills the text area with the text to be translated. Next Playwright waits for the translation to appear (the box 'Translations of  auto' in the screenshot below). The result is selected and saved. First I select span.VIiyi (a CSS selector) and within that span I select ../span/span/span (an XPATH selector). After the result is obtained, I close the browser and return it


When you start the service, you will get something like
 [2021-01-24 09:16:38 +0100] [3278] [INFO] Goin' Fast @ http://0.0.0.0:5000  
[2021-01-24 09:16:38 +0100] [3278] [INFO] Starting worker [3278]
Next you can test the service. You can do this with 
 curl 'http://localhost:5000/translate?sl=nl&tl=en&translate=auto'
It will translate the Dutch (sl=nl) 'auto' (translate=auto) to English (tl=en). The result will be car.
 {"translation":"car"}  

Performance

Of course performance tests come with a big disclaimer. I tried this on specific hardware, on a specific OS, etc. You will not be able to exactly reproduce these results. Also the test I did was relatively simple just to get an impression. I additionally logged the responses since the script I used did not include proper exception handling.

I used wrk, an HTTP benchmark tool on the service.
 wrk -c2 -t2 -d30s --timeout=30s 'http://localhost:5000/translate?sl=nl&tl=en&translate=auto'
WRK used 2 threads and kept 2 connections open at the same time (2 concurrent requests). Per request I set the timeout to 30s. This timeout was never reached.

This gave me average results of 2.5s per request. At 4 concurrent requests, this became 3s on average. At 8 concurrent requests this became 4.5s on average and started to give some errors. A 'normal' API can of course do much better. I was a bit surprised though Playwright could handle 8 headless browsers simultaneously on an 8Gb Ubuntu VM which also had PyCharm open at the same time (knowing what a memory-hog Chrome can be). I was also surprised Google didn't start to bother me with Capcha's yet.

Tips for development

Automatically generate scripts

You can use Playwright to automatically generate scripts for you from manual browser interactions. This can be a good start of a script. See for example here. The generated scripts however do not contain smart logic such as waiting for certain elements in the page to appear before selecting text from other elements.

Use browser developer tools

In order to automate a browser using Playwright, you need to select elements in the web-page. An easy way to do this is by looking at the developer tools which are present in most web-browsers nowadays. Here you can easily browse the DOM (document object model, the model on which the browser bases what it shows you). This allows you to find specific elements of interest to manipulate using Playwright.


Approximate human behavior

One of the dangers of creating screen scrapers is that if the site changes, your code might not work anymore. In my sample script I used specific identifiers like Dwvecf and VIiyi and queried for elements based on the sites DOM. When you look at a website yourself, you select elements in a more visual way. The better you can approximate the way a human would interact with a website, the more stable your script will be. For example, selecting the first textarea on the site is more stable then expecting the result to be in span.VIiyi and in that element under span/span/span. 

The right tool for the job

If an API is available and you can use it directly, that is of course preferable to using a website scraper since an API is made for automated interaction and a web-site is made for human interaction. You usually get much better performance and stability when using an official API instead. Playwright includes an API to monitor and modify HTTP and HTTPS requests done by the browser. This might help you in determining back-end APIs so you can try if you can use them directly.

When using a tool like Playwright to automate browser interaction in for example tests for custom developed applications, you can get better stability since you know what will change when in the site and can make sure the automation scripts keep working. When you're using Playwright against an external website, you will have less control. Google will not inform me when they change https://translate.google.com and this script will most likely break because of it.

Java Agent: Rewrite Java code at runtime using Javassist

$
0
0

There are situations where you want to change Java code not at the source code level but at runtime. For example when you want to instrument code for logging purposes but do not want to change the source code (because you might not have access to it). This can be done by using a Java Agent. For example, Dynatrace uses a Java agent to collect data from inside the JVM. Another example is the GraalVM tracing agent (here) which helps you create configuration for the generation of native images. Logging is one use-case but you can also more dramatically alter runtime code to obtain a completely different runtime behavior.

This blog post is not a step by step introduction for creating Java Agents. For that please take a look at the following. In this blog post I have created a Java agent which rewrites synchronized methods to use a ReentrantLock instead (see here). The use-case for this is to allow applications to use Project Loom's Virtual Threads more efficiently.

You can find the code of the agent here.

The Java Agent

When I created the agent, I encountered several challenges, which I listed below and how I dealt with them. This might help you to overcome some initial challenges when writing your own Java Agent.

Keep the dependencies to a minimum

First, the JAR for the agent must include its dependencies. You often do not know in which context the agent is loaded and if required dependencies are supplied by another application. The maven-dependency-plugin with the descriptor reference jar-with-dependencies helps you generate a JAR file including dependencies.

When you include dependencies however in the JAR containing also your agent, they might cause issues with other classes which are loaded by the JVM (because you usually run an application or even application server). The specific challenge which I encountered was that I was using slf4j-simple and my application was using Logback for logging. The solution was simple; remove slf4-simple. More generally; use as few external dependencies as viable for the agent. I used only javassist. I needed this dependency to easily do bytecode manipulation.

Trigger class transformation for every class

There are several solutions available to obtain the correct bytecode. Be careful though that your bytecode transformer is triggered for every class that is loaded when it is loaded. Solutions which only look at currently loaded classes (instrumentation.getAllLoadedClasses()) or a scan based on reflection utils like javassist (example here) won't do that. You can register a transformer class that will be triggered when a specific class is loaded, but if you want your agent to be triggered for every class, it is better to register a transformer without having a specific class specified; instrumentation.addTransformer(new RemsyncTransformer());. The agent will be the first to be loaded when using the premain method / the javaagent JVM switch when starting your application.

Of course, if you want to attach the agent to an already running JVM (using the agentmain method), you still need to process the already loaded classes.

Obtain the bytecode

I've seen several examples on how to obtain the bytecode of the class to edit in the transformer class. The signature of the transform method of transformer class is: 

 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,  
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException

It is tempting to use the loader, className and classBeingRedefined to obtain the relevant CtClass instance. The CtClass instance can be edited using javassist. For example by doing something like: 

 String targetClassName = className.replaceAll("\\.", "/");  
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(targetClassName);

I'm however not a great fan of string parsing / editing to obtain the correct name of a class and then fetch it. The classfileBuffer contains the bytecode of the class being loaded and this can be edited directly. I implemented this like below (based on this which contains a more elaborate explanation).

  private final ScopedClassPoolFactoryImpl scopedClassPoolFactory = new ScopedClassPoolFactoryImpl();  

ClassPool classPool = scopedClassPoolFactory.create(loader, ClassPool.getDefault(),ScopedClassPoolRepositoryImpl.getInstance());
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

Editing the bytecode

I used the following code to add a private instance variable, remove the synchronized modifier and wrap the contents of the method in a try/finally block:

 try {  
ctField = ctClass.getDeclaredField("lockCustomAgent");
} catch (NotFoundException e) {
ctClass.addField(CtField.make("final java.util.concurrent.locks.Lock lockCustomAgent = new java.util.concurrent.locks.ReentrantLock();", ctClass));
}

method.instrument(new ExprEditor() {
public void edit(MethodCall m) throws CannotCompileException {
m.replace("{ lockCustomAgent.lock(); try { $_ = $proceed($$); } finally { lockCustomAgent.unlock(); } }");
}
});

modifier = Modifier.clear(modifier, Modifier.SYNCHRONIZED);
method.setModifiers(modifier);

What this did was change the following Java code

   synchronized void hi() {  
System.out.println("Hi");
}

to the following

   final java.util.concurrent.locks.Lock lockCustomAgent = new java.util.concurrent.locks.ReentrantLock();  

void hi() {
lockCustomAgentStatic.lock();
try {
System.out.println("Hi");
} finally {
lockCustomAgentStatic.unlock();
}
}

This is exactly as described here at 'Mitigating limitations'.

Generate your JAR manifest

A manifest file helps the JVM to know which agent classes to use inside the JAR file. You can supply this file and package it together with the compiled sources in the JAR or let it be generated upon build. I choose the second option. The following will generate a manifest indicating which agent class needs to be started by the JVM. 

<plugin>  
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>nl.amis.smeetsm.agent.RemsyncInstrumentationAgent</Premain-Class>
<Agent-Class>nl.amis.smeetsm.agent.RemsyncInstrumentationAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>

Finally

The agent described here illustrated some of the options you have for editing bytecode. I noticed though when creating this agent, that there are many situations where the agent cannot correctly rewrite code. This is probably because my rewrite code is not elaborate enough to deal with all code constructs it may encounter. An agent is probably only suitable for simple changes and not for complex rewrites. One of the strengths of using a method like this is though that you do not need to change any source code to check out if a certain change does what you expect. I was surprised at how easy it was to create a working agent that changes Java code at runtime. If you have such a use-case, do check out what you can do with this. PS for Project Loom, just using virtual threads and rewriting synchronized modifiers did not give me better performance.

You can find the code of the agent here.

Java Security: Open Source tools for use in CI/CD pipelines

$
0
0

It is often expected of a DevOps team to also take security into consideration when delivering software. Often however, this does not get the attention it deserves. In this blog post I'll describe some easy to use, CI/CD pipeline friendly, open source tools you can use to perform several checks during your Java software delivery process which will help you identify and fix issues with minimal effort.

You can view my sample project here which implements all these tools. There is also a docker-compose.yml file supplied. SonarQube en Jenkins however do not come preconfigured in this setup. You can look at the Jenkinsfile (pipeline definition) to see what Jenkins configuration is required (installing plugins and creating credentials). 

This is provided as an overview and small example only. It is not meant as a walkthrough to setup a CI/CD environment or a manual how to use the different tools. Also the mentioned tools are not all the tools which are available but a set of tools which are freely available, popular and which I managed to get working without too much effort. Using them together allows you to improve the security of your Java applications during your software delivery process and provide quick feedback to developers. This can also help increase security awareness. When you have some experience with these tools you can implement more strict policies (which can let a build fail) and quality gates.

Static code analysis

What does it help you solve?

Static application security testing (SAST) helps you identify several issues in your source code by looking at the code itself before compilation. Some issues which static analysis can help prevent are;

  • Exposed resources
    Static analysis tools can help detect exposed resources. These might be vulnerable to attacks.
  • Hardcoded keys or credentials
    If keys or passwords are hardcoded inside your  application, attackers might be able to extract them by for example decompiling the Java code
  • Stability issues
    This can be related to memory leaks such as not closing resources which will no longer be used. This can also be related to performance. Bad performance can lead to stability issues. An attacker might purposefully try to bring your application down by abusing performance issues. 

Which tools can you use?

When talking about Java applications, the following tools are free to use to perform static analysis.

  • PMD (here)
    PMD can be used as a Maven or Gradle plugin (and probably also in other ways). It has a Jenkins plug-in available and the SonarQube Maven plugin can be supplied with parameters to send PMD reports to SonarQube. PMD can find a small number of actual security vulnerabilities. It can provide quite a lot of performance related suggestions.
  • SpotBugs (here)
    SpotBugs is the spiritual successor of FindBugs. It can (like PMD) easily be integrated in builds and CI/CD pipelines. The SonarQube Maven plugin also supports sending PMD reports to SonarQube without additional plugin requirements. SpotBugs can find bugs in the area of security and performance among others (here).
  • FindSecBugs (here)
    This is an extension of SpotBugs and can be used similarly. It covers the OWASP TOP 10 and detects /classifies several security related programming bugs.
In this example I did not use a Dockerfile but Dockerfiles can also not conform to best practices making them vulnerable. If you want to do this, you can take a look at hadolint.

Dynamic Application Security Testing (DAST)

Some things cannot easily be automatically identified from within the code. For example, how the application looks on the outside. For this it is not unusual to perform a penetration test. Penetration tests can luckily be automated!

What does it help you solve?

  • Scan web applications and analyse used headers
  • Check exposed endpoints and security features of those endpoints
  • Check OpenAPI / SOAP / Websocket communication/services
  • Check traffic between browser and service

Which tools can you use?

  • OWASP ZAP (Zed Attack Proxy)
    Can run in a standalone container, started from a Maven build using a plugin, configured as a tool within Jenkins. You can install a plugin (here) in SonarQube to visualize results and use the HTML publisher plugin in Jenkins to show the results there. OWASP ZAP can also be used together with Selenium to passively scan traffic between the browser and the service or actively modify requests.

I did not take a look at other tools to do similar things yet. 

Dependency analyses

What does it help you solve?

  • Java programs use different libraries to build and run. You can think of frameworks, drivers, utility code and many other re-usable assets. Those dependencies can contain vulnerabilities. Usually for older libraries, more vulnerabilities are known. Knowing about vulnerabilities in specific versions of libraries and their severity, can help you in prioritizing updating them.
Which tools can you use?

  • OWASP Dependency Check (here)
    Can run from a Maven build and supply data to Jenkins and SonarQube. Also records CVSS scores (Common Vulnerability Scoring System) for risk assessments.

Container image vulnerability scanning

What does it help you solve?

  • A Java program can run in a container on a container platform. A container is usually created by using a base image and putting an application inside. The base image contains operating system libraries or other software (such as a JVM) which can contain vulnerabilities.

Which tools can you use?

  • Anchore Engine (here).
    Anchore Engine is available as a container image. Once started, it will download information from various vulnerability databases. You can request a certain image to be analyzed. It has good integration with SonarQube and Jenkins. Read my blog posts about using Anchore Engine here and here.

OWASP ZAP: A quick introduction

$
0
0

OWASP ZAP or Zed Attack Proxy is an open source dynamic application security testing (DAST) tool. It is available here and has a website with documentation here. I recently encountered it when looking for open source security test tools to embed in a CI/CD pipeline (here). I was surprised by how versatile this tool is. In this blog post I'll summarize several ways how you can use it. 

Demo application

I've used a small Spring Boot service to illustrate the different ways you can use OWASP ZAP. You can find it in DockerHub here and in GitHub here

To create a Docker network and start the application, do the following;

 docker network create demo-network
 docker run --network demo-network --name spring-boot-demo -p 8080:8080 maartensmeets/spring-boot-demo  

ZAP CLI

OWASP ZAP CLI can be run from a container (read here). You can for example execute a scan like listed in the command below. Mind that by default only High severity issues are displayed. That's why there is '-l Low' at the end.

 docker run --network demo-network -i owasp/zap2docker-stable zap-cli quick-scan --spider --self-contained --recursive --start-options '-config api.disablekey=true' http://spring-boot-demo:8080/rest/demo/ -l Low  

This can give you output like;

 [INFO]      Starting ZAP daemon  
[INFO] Running a quick scan for http://spring-boot-demo:8080/rest/demo/
[INFO] Issues found: 1
+---------------------------------------+--------+----------+-----------------------------------------+
| Alert | Risk | CWE ID | URL |
+=======================================+========+==========+=========================================+
| X-Content-Type-Options Header Missing | Low | 16 | http://spring-boot-demo:8080/rest/demo/ |
+---------------------------------------+--------+----------+-----------------------------------------+
[INFO] Shutting down ZAP daemon

Using a GUI

Webswing allows you to run OWASP ZAP from a container with a GUI which you can open in a browser. Read here

  docker run -u zap -p 8081:8080 -p 8090:8090 --network demo-network -i owasp/zap2docker-stable zap-webswing.sh  

Now you can go to localhost:8081/zap to open the GUI. 

Quickscan

You can perform an automated scan from the GUI to quickly scan a specific endpoint. This will yield results similar to the zap-cli example above.


Proxy requests

You can also use port 8090 of the container as proxy. URLs which pass the proxy will be listed in the GUI and you can easily attack all detected endpoints. 


It will ask you to accept the self-signed ZAP certificate. You can import the ZAP root CA certificate to permanently to avoid the warning in the future. Read the following here on how to do that.


Next you can open the service endpoint via the ZAP proxy



You can also see the request and response in the ZAP GUI.


As you can see, this gives more information than only a scan of a specific endpoint since OWASP ZAP can now look at the traffic between browser and service. Here it detects an additional header missing. It is also possible to set breakpoints and alter requests. In addition, there is scripting support available. See for example here.

Embedded in a CI/CD process

There are various ways in which you can embed an OWASP ZAP scan in your CI/CD process. Since it is a DAST scan, it requires a running application. Also ZAP itself needs to be running in order to execute scans. The specific method is highly dependent on your environment. 

Install

Depending on your environment you can spin up ZAP as a container or install ZAP yourself by for example using the Linux installer which you can find here

Start and stop

Ideally, you want to start ZAP, execute a scan and clean-up. This in order not to have it claim resources while not in use (and saving you money, especially in a cloud environment).

You can use a plugin such as this one to start and stop a ZAP container or a local installation. If the plugin does not suffice, it is easy to script it yourself as part of your pipeline.

Execute

Once your application and ZAP are running, there are various ways to initiate a scan. You can access its API (directly, using the CLI or by using for example a plugin) and ask it to do scans of specific endpoints. Using a plugin is relatively easy.

You can embed a ZAP scan in a Selenium test so you can first proxy requests and than analyse the proxied traffic (passive) and attack those endpoints (active). See for example the following presentation here on how to set that up. This requires a bit more work.

Process ZAP results

If you want to integrate the results of the scan in for example SonarQube, you can use the following plugin here. You can also integrate it in Azure DevOps by transforming the result XML file to a format Azure DevOps understands (see here).

Example

I created a docker-compose.yml file which starts ZAP and my test application here. Next from the pom.xml file (Maven, it is a Java application) I use a plugin during the build process to access ZAP using the API and initiate a scan. The result is published to Jenkins and imported in SonarQube. A setup such as this one is of course not viable for real-life use since the ZAP spin-up and spin-down are not part of it (I start everything at the same time when I do docker-compose up -d).

Finally

I was happily surprised by how many integration options OWASP ZAP has and the different ways you can run ZAP. It has a GUI, a CLI and a powerful API. This makes it easy to embed it into any CI/CD pipeline to dynamically scan endpoints. The proxy option in combination with an automated GUI test is very powerful. It allows ZAP to first do a passive scan of traffic (identifying endpoints, analyzing requests/responses) and with the information obtained, to do an active attack. Such elaborate scans can take a while though, but they can provide a lot of information on potential vulnerabilities. In my opinion, there is no good reason not to embed OWASP ZAP in your software delivery process to make the world a little bit safer by performing automated security tests.

GitHub Actions: A first impression

$
0
0

I'm a regular user of GitHub. Recently I discovered GitHub also has a build-in CI/CD workflow solution called GitHub Actions. Curious about how this would work I decided to try it out. I had previously build a Jenkins Pipeline to perform several static and dynamic application security tests on a Java project and decided to try and rebuild this pipeline using GitHub Actions. This blog described my first experiences and impressions.


First impressions

No external environment required

One of the immediate benefits I noticed was that the Jenkins Pipeline requires a Jenkins installation to be executed. GitHub Actions work on their own in your GitHub repository so no external environment is required. Do mind that GitHub Actions have some limitations. If you want to use them extensively, you will eventually probably need to pay.

Integrated with GitHub

GitHub Actions are of course specific to GitHub and integrate well with repository functionality. Because of this, you would probably only use them if your source code is in GitHub. There are quite a lot of options to have a workflow triggered. For example on a push or pull request, on a schedule or manually. Because GitHub Actions are integrated with GitHub and linked to your account, you will get automatic mails when a workflow succeeds or fails.

If you want to integrate Jenkins with GitHub, the communication is usually done via webhooks. This requires you to expose your Jenkins environment in a way GitHub can reach it. This can be challenging to do securely (might require you to punch holes in your firewall or use a polling mechanism). Also if you want to be informed of the results of a build by e-mail, you need to define mail server settings/credentials in Jenkins. For GitHub Actions this is not required.

Reporting capabilities

My Jenkins / SonarQube environment provide reporting capabilities. At the end of a build you can find various reports back in Jenkins and issues are created in SonarQube. In SonarQube you can define quality gates to let the build fail or not.

Using GitHub Actions, this works differently. Letting a build fail is something to explicitly script instead of configured using a GUI. At the end of a build you can export artifacts. These can contain reports but you need to open them yourself. Another option is to publish them to GitHub Pages (see here). Thus no 'out of the box' capability to do reporting for GitHub Actions.

GitHub has so-called "Issues" but these appear not to be meant to register individual findings of the different static code analysis tools. They work better to just register a single issue for a failed build. Of course, there are GitHub Actions available for that.

Extensibility

To be honest, I did not dive into how to create custom pipeline steps in my Jenkins environment. I noticed you can code some Groovy and integrate that with a Pipeline but it would probably require quite a time investment to obtain the required knowledge to get something working. For GitHub Actions, creating your own Actions is pretty straightforward and described here. You create a repository for your Action, a Dockerfile and a yaml describing the inputs and outputs of the Action. Next you can directly use it in your GitHub workflows. As the Dutch would say; 'een kind kan de was doen' (literally: 'a child can do laundry'. It is a saying which means: 'it's child's play')

Challenges rebuilding a Jenkins Pipeline


I had created a simple Jenkins Pipeline to compile Java code, perform static code analysis, create a container, deploy it to Docker Hub, scan the container using Anchore Engine, perform an automated penetration test using OWASP ZAP, feed the results to SonarQube and check a Quality Gate.

The main challenges I had with rebuilding the pipeline were working with containers during the build process and how to deal with caching.

Starting and stopping containers

The main challenges I had when building the Jenkins Pipeline was that Jenkins was running inside a container and I wanted to spawn new containers for Anchore, ZAP and my application. Eventually I decided to use docker-compose to start everything upfront and do a manual restart of my application after the container image in Docker Hub was updated. This challenge is similar when running Jenkins on K8s.

Using GitHub Actions, the environment is different. You choose a so-called runner for the build. This can be a self-hosted environment but can also be GitHub hosted. GitHub offers various flavors of runners. In essence, this gives you for example a Linux VM on which you can run stuff, start and stop containers and more or less do whatever you want to perform your build. The Jenkins challenge of starting and stopping containers from within a container is just not there! 

For GitHub Actions, you should be aware there are various ways to deal with containers. You can define service containers which will start when your workflow starts. You can also execute certain actions in a container you specify with the 'uses' keyword. When these do not suffice, you can call docker directly yourself from a run command or by calling an external script. I chose the last option in my workflow. See my script here for starting the OWASP ZAP scan in a container.

Caching artifacts

To reduce build time, you can cache artifacts such as Maven dependencies but also of container images. For the container images you can think of images cached by Google Jib or images cached by the Docker daemon. I tried caching images from the Docker daemon but that increased my build time. Most likely because of how the cache worked. Every image which was saved in the cache and later restored, included all underlying layers. Probably certain layers are part of multiple images so they are saved multiple times. It appeared downloading only the required layers was quicker than caching them and restoring them  on a next build. If you are however more worried about network bandwidth than storage, you can of course still implement this. For my project, caching the Maven and Jib artifacts caused my build time to go from 7m 29s to 4m 45s so this appeared quite effective. Probably because artifacts are only downloaded once (no doubles) and there are many small artifacts which all require creating a connection to a Maven repository. Applying a saved repository was quicker than downloading in this case.

Setting environment variables

Setting environment variables in GitHub Actions was not so much of a challenge but in my opinion, the method I used felt a bit peculiar. For pushing an image from my Maven build to Docker Hub, I needed Docker Hub credentials. Probably I should have used a Docker Hub access token instead for added security but I didn't.

I needed to register my Docker Hub credentials in GitHub as secrets. You can do this by registering repository secrets and refer to them in your GitHub Actions workflow using something like: ${{ secrets.DOCKER_USERNAME }} .

From these secrets, I created environment variables. I did this like; 
   steps:  
- name: Set environment variables
run: |
echo "DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}">> $GITHUB_ENV
echo "DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}">> $GITHUB_ENV

I could also have specified the environment at the step level or at the workflow level using the env keyword like;

  env: # Set environment variables
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

The first option is very flexible since you can use any bash statement to fill variables. The second option however is more readable.

Getting started with GitHub Actions

Creating GitHub Actions is relatively simple. You create a file called .github/workflows/main.yml in your GitHub repository. You can use a provided starters from GitHub specific to a language or create one from scratch for yourself.

When you have created such a file, you can go to the Actions tab in GitHub after you have logged in;


You can in my case open the CI workflow and run it manually (I choose to do it manually but as mentioned, you can use other triggers). When performing a build manually, you can define inputs but I did not use that functionality.


When you open a build, you can view logging and generated artifacts (under summary). In my example I published the test reports as artifacts and pushed my created container to Docker Hub. I was not so much interested in the generated JAR file.


Mind that I didn't need to setup any environment on my own but just used the freely available GitHub Actions functionality.

Pipeline code

You can browse the pipeline here.
 name: CI  

# Controls when the action will run.
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-20.04

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Set environment variables
run: |
echo "DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}">> $GITHUB_ENV
echo "DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }}">> $GITHUB_ENV

# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout sources
uses: actions/checkout@v2

# Runs a single command using the runners shell
- name: Setup Java 11
uses: actions/setup-java@v1
with:
java-version: 11

#- name: Cache Docker images
# uses: satackey/action-docker-layer-caching@v0.0.11
# # Ignore the failure of a step and avoid terminating the job.
# continue-on-error: true

- name: Cache Maven packages and Google Jib cache
uses: actions/cache@v2
with:
path: |
~/.m2
~/.cache/google-cloud-tools-java/jib
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2

- name: Static checks
run: mvn --batch-mode --update-snapshots dependency-check:check pmd:pmd pmd:cpd spotbugs:spotbugs

- name: Publish image to Docker Hub
run: mvn --batch-mode --update-snapshots compile jib:build

- name: Anchore scan
uses: anchore/scan-action@v1
with:
image-reference: "docker.io/maartensmeets/spring-boot-demo"
fail-build: true

- name: Start service
run: |
docker network create zap
docker run --pull always --name spring-boot-demo --network zap -d -p 8080:8080 docker.io/maartensmeets/spring-boot-demo

- name: OWASP ZAP scan
run: |
# make file runnable, might not be necessary
chmod +x "${GITHUB_WORKSPACE}/.github/zap_cli_scan.sh"
# run script
"${GITHUB_WORKSPACE}/.github/zap_cli_scan.sh"
mv owaspreport.html target

- name: 'Publish Test Report'
if: always()
uses: actions/upload-artifact@v2
with:
name: 'test-reports'
path: |
target/*.html
target/site/
./anchore-reports/
As you can see, the workflow is pretty readable, except maybe the OWASP ZAP part. The different steps have already been described generally before. It was a shame I did need to fall back a couple of times on running direct shell commands and scripts instead of completely relying on re-usable GitHub Actions. Improving that might be something for the future.

It is nice you can generate a badge to use in your GitHub README file indicating the status of the build.

Finally

The good

GitHub Actions are out of the box CI/CD workflow functionality which you can use (with some limitations) when you have a free GitHub account. Because it is out of the box functionality, you are not required to setup your own CI/CD environment to achieve the same. It runs within GitHub and is quite nicely integrated with for example repository events and secrets functionality. There are a lot of GitHub Actions already available and creating/using your own is a piece of cake. The workflows can be triggered in several ways and you can fall back on manual scripting if required. The provided runner images contain a Docker installation so working with containers also works nicely (which can be challenging when your own CI/CD environment runs within a container). You can even use your own runner in for example a different cloud to get improved performance and more resources for the runner if you need it. To summarize, GitHub Actions are powerful, flexible and well integrated with other GitHub provided functionality.

The bad

GitHub Actions are great when you have your project in GitHub and don't want to setup your own CI/CD environment. When you are for example using a private repository outside of GitHub, using GitHub Actions is not the obvious choice.

I was disappointed by the out of the box reporting capabilities. I could generate reports, however I could not publish them anywhere easily without introducing additional dependencies. Scripting the generation of GitHub Issues also did not seem like it was the way to go to solve this. I could use GitHub Pages, however, a link would not be quickly accessible from the build itself (like I was used to in Jenkins) and the logic on how to deal with reports from different builds/branches and collecting the reports in a single accessible page, is something to create for yourself (or use a specific provider). Much of the functionality of a tool like SonarQube was also something I missed, such as fine grained issue management and the ability to define quality gates.

GitHub Actions and SonarCloud

$
0
0

GitHub Actions allow you to do most CI/CD tasks for free, directly from your GitHub repository. One of the challenges however is that there is no build-in facility like for example SonarQube to manage code quality. Luckily, SonarSource provides SonarCloud; a SonarQube SaaS offering which is free for public projects! It is also easy to feed SonarCloud from GitHub Actions. In this blog post I'll describe how you can do this.

Limitations

There are of course some limitations on usage for the free GitHub and SonarCloud accounts. Next to that however, SonarCloud does not allow 3rd party plugins. It is a SaaS offering and allowing 3rd party plugins would cause an additional burden on managing the environment and in addition possible licensing issues. For some code quality aspects however, using 3rd party plugins is currently the only option. Examples of these are the OWASP Dependency-Check and OWASP ZAP. Processing output of those tests is currently not supported in SonarCloud. You can however feed it with SpotBugs (the spiritual successor of FindBugs), PMD and code coverage data. To work around the 3rd party plugin limitation, you could possibly convert the Dependency-Check and ZAP data and merge it with the SpotBugs/PMD output and feed that to SonarQube. I haven't tried that yet however.

GitHub repository

I used the following GitHub repository with some Java code to generate code quality information. In order to do that I had the following entries in my pom.xml file;


SonarCloud configuration

In order to feed data to SonarCloud, some preparations needed to be done on that side. First login to SonarCloud using your GitHub account.


Next you have to authorize SonarCloud:



You can now add a GitHub organization you are using to SonarCloud by clicking + next to your account.


I chose my personal organisation. SonarCloud will be installed as a GitHub App for that organization.


You can grant SonarCloud access to your repository


In SonarCloud you can now create an organisation


And analyse a new project



When you click Set Up, SonarCloud suggests doing that with GitHub Actions which is of course fine by me.


In your GitHub repository, you need to create a token so GitHub can access SonarCloud:


GitHub Actions

Conveniently, SonarCloud provides an instruction on what you need to do in order to allow the GitHub Actions to feed SonarCloud. These include updating your pom.xml file to specify the target for the SonarSource plugin and creating a workflow or adding some actions specific to the analysis. Specific are the shallow clone option, the SonarCloud artifact cache and of course the build and analyse step.


In the example workflow given by SonarCloud, the build is triggered on every commit. I changed this to do it manually. You can browse my workflow definition here.





After the results have been fed to SonarCloud, you can browse them there;


OWASP Dependency-Check to SonarCloud

$
0
0

SonarCloud is a hosted SonarQube SaaS solution which does not allow 3rd party plugins to be installed. This puts some limitations on the kind of data you can put in SonarCloud. For Java this is limited to Checkstyle, PMD and SpotBugs results. OWASP provides a Dependency-Check plugin to identify vulnerable dependencies in for example your pom.xml file. In this blog post I'll show how to get OWASP Depedency-Check data in SonarCloud without using a 3rd party plugin! Disclaimer: this solution has been created in very little time (~2 hours) and has not been seriously tested, optimized or used in production environments. Use at your own risk!

Method used

SonarCloud can import CheckStyle, PMD, SpotBugs result data. The output XML files which are generated by those plugins, conform to a specific format specified in XSDs.

The Dependency-Check results also have an XSD (here).

I checked out the different XSDs and decided the PMD XSD was easiest to use. I created an XSLT transformation to transform the Dependency-Check result to a PMD result file and send that to SonarCloud.

Build process

In my pom.xml first the Dependency-Check report needed to be generated before I could perform a transformation. When performing the transformation, I needed to have XSLT 2.0 support to easily get the current date/time for a timestamp. This required an additional dependency. You can take a look at my pom.xml file here. I executed a "mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.java.pmd.reportPaths=target/pmd.xml,target/dependency-check-report-pmd.xml" to generate the report and send it to SonarCloud. Notice you can specify a comma separated list of PMD files to send. Checkout my GitHub workflow for more details on the exact build process and if you're interested, this blog post on how I setup GitHub Actions and SonarCloud interaction.


Transformation

I created the following transformation (which you can download here):


The challenges here were:
  • the current-dateTime function which required XSLT 2.0
  • transforming the CVSS3 rating to a PMD severity rating. PMD uses 1 for highest severity and 5 for lowest. CVSS3 uses 10 for highest and 0 for lowest.
  • the file the issue refers to is required to exist in your code. Supplying the JAR file which causes the issue did not work so I set it to my pom.xml
  • required fields like line number. 0 is not allowed so I set them to 1
  • I did not find the externalInfoUrl in SonarCloud in a location I could click on it. Now you have to go to the NVD site yourself and look for the issue.
Result

The result of feeding the resulting PMD results file to SonarCloud was that I could see the issues with correct severity on SonarCloud with their description and CVE code.


This does look a bit worse though than using a 'native' Dependency-Check report and 3rd party plugin in SonarQube. For example, tags are missing and they are reported as "Code Smell" instead of "Vulnerability". Also more vulnerabilities are reported when using this method compared to the SonarQube setup. I have not looked into this in more detail but since they refer to the same file, fixing that will probably get rid of all issues.


Jenkins: Obtaining and displaying credentials

$
0
0

Jenkins is a solid CI/CD platform which has proven itself over the years. Many organizations use it to build, test and deploy their applications. In Jenkins it is possible to define credentials or to use an external credential store. You can then use these inside your pipelines and jobs. Direct access to credentials can be limited. Even with limited access, there are still various ways in which you can extract credentials. 

This blog post illustrates how you can display credentials in log files as base64 encoded strings so they are not masked and you can easily copy / paste / base64 decode them to obtain and (ab)use them. The method described is not specific to Jenkins but can also be used in various other CI/CD platforms on-premises and in the cloud (such as GitHub Actions and Bamboo). 

Note: this is not meant as an encouragement to break rules or laws. Often legislation does not allow you to try and access systems you are not officially authorized to. It is meant to create awareness and to allow you to think about if and how you might want to prevent this in your own CI/CD platform.

Pipeline as code

You often see pipeline definitions which are shipped together with the code which needs to be build, tested, deployed (think Jenkinsfile or GitHub Actions). These pipelines contain commands which are allowed to use credentials. These credentials are often available in environment variables so they can easily be used for example to execute deployments. Since the pipeline is bundled with the application, you can perform version control on it and easily put the responsibility for maintaining the pipeline at the team which builds and maintains the code. Credentials are often defined as secrets within the platform. See for example below for the definition of a Jenkins secret.


Creating a new pipeline

When I'm able to create a new Jenkins pipeline or update an existing pipeline I can obtain credentials defined in the CI/CD platform.

I can define a pipeline outside of version control as part of the job definition. I do require access to an agent to execute the pipeline though.


When I'm required to change the pipeline code inside a version control system, my actions will be far more easy to trace and I might not be so eager to try this. Creating and removing a job after having used it to obtain credentials, often leaves few traces. 

Access to Jenkins credentials

Limit UI and API options

Within Jenkins, you can use matrix based security to limit access to credentials


This will limit your options in the Jenkins UI and via the Jenkins API

A limited user cannot create or edit credentials.

Mask credentials in logging

Printing credentials in the Jenkins logging by using a plain echo command, will give you a warning and mask the credential. Even when wrapping the displaying of the credential in a more complicated command, Jenkins still masks it (although without warning). When however base64 encoding the credentials, Jenkins doesn't recognize them anymore and displays the base64 encoded credentials which you can easily decode using unsafe websites such as this one. Thank you very much!


Protect your credentials!

Below are some suggestions which might help you prevent these kind of issues. Mind that they are partially at odds with the DevOps mindset and the pipeline as code principle! They might also limit your options in putting the responsibility for the build/deploy process at your developers. Do not apply without carefully weighing the pro's and con's.
  • Use the principle of least privilege when configuring authorizations in your CI/CD platform. For example only allow certain Jenkins pipelines to use a specific build agent and disallow changing existing jobs or creating new ones which can also use an existing agent.
  • Separate build and deployment jobs and responsibilities. Maybe even use different tools or environments for them. This way, when you have access to a build pipeline during development, you can prevent someone to easily extract credentials which are used for production.
  • Explicitly request credentials upon deployment as manual input. This does not prevent malicious pipeline code to be executed and for example display the credentials in log files (or worse, send them to a remote endpoint), but that pipeline is executed under your watch, which makes it less likely someone else will be able to listen in.
  • Limit access to pipeline code / jobs / flow definitions (or whatever your CI/CD platform calls them).
  • Try and prevent using environment variables for holding credentials (when your CI/CD platform and application deployment tooling supports this).
Viewing all 142 articles
Browse latest View live


Latest Images