Skip to main content

Building a secure CI/CD pipeline with GitHub Actions for your Java Application

Written by:

June 27, 2022

0 mins read

GitHub Actions has made it easier than ever to build a secure continuous integration and continuous delivery (CI/CD) pipeline for your GitHub projects. By integrating your CI/CD pipeline and GitHub repository, GitHub Actions allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository or deploy merged pull requests to production. And by integrating Snyk into your GitHub CI/CD, you can automate security scanning as part of your build cycle prior to production.

Setting up a Github Actions CI pipeline for Java

GitHub Actions workflows are YAML files in the .github/workflows folder. If you do not have a workflow, or want to add a new one, select Actions and New workflow.

wordpress-sync/blog-github-actions-workflows

For this Java Spring-Boot project, I created a CI pipeline using maven as follows:

1name: Java CI with Maven
2
3on:
4 push:
5   branches: [ master ]
6 pull_request:
7   branches: [ master ]
8 workflow_dispatch:
9
10jobs:
11 build:
12
13   runs-on: ubuntu-latest
14
15   steps:
16   - uses: actions/checkout@v2
17   - name: Set up JDK 17
18     uses: actions/setup-java@v1
19     with:
20       java-version: 17
21   - name: Build with Maven
22     run: mvn -B package --file pom.xml

This action will run on every push or pull request on the master branch. It is based on ubuntu and checks out the repository, while using the setup-java GitHub Action — with Java 17 and Maven — to build the Java jar file. If you're familiar with the syntax, this workflow is relatively straightforward, but you can refer to the GitHub Actions documentation for a complete overview of the possibilities.

Integrating Snyk in your Github CI/CD

With Snyk, you can integrate security testing for your new Java project. There are two primary methods for doing this. However, let’s make sure two things are set up first. Begin by logging into your Snyk account, creating one for free if you haven't already. Then set your API key as a secret SNYK_TOKEN for your GitHub repository.

Option 1: Integrate the Snyk CLI in a build step of your GitHub Action

You can use the Snyk CLI to automatically run security scans inside your current build. Follow the steps described below — including the few extra steps afterBuild with Maven — to get started.

1 - name: Set up Node 14
2   uses: actions/setup-node@v3
3   with:
4     node-version: 14
5 - name: install Snyk CLI
6   run: npm install -g snyk
7 - name: run Snyk Open Source Test
8   run: snyk test
9 - name: run Snyk Code Test
10   run: snyk code test

Set up NodeJS version 14 and download the Snyk CLI using npm. Next, analyze your dependencies with Snyk Open Source, and use Snyk Code to scan your custom code for vulnerabilities.

Then, declare SNYK_TOKEN as the environment variable containing your API key. You can refer to the secret SNYK_TOKEN you set up earlier, or feel free to reuse the full code example of the GitHub Action.

1env:
2 SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

The benefit of this approach is that you only have to compile and build your application once, which can save a lot of time when building large applications. The downside is the steps are used in series, and might not be efficient if one of the later steps fails.

Option 2: Use the predefined GitHub Actions from Snyk to create your CI/CD pipeline

Snyk created a set of GitHub Actions to check for vulnerabilities in your projects. The required action depends on your language or build tool. For example, we’ll use the maven-based GitHub Action shown below for our Java project.

The predefined actions ensure you have the correct prerequisites to build your application and include the latest CLI version to scan your code. Even though you may rebuild your applications several times, these actions can run individually and in parallel — a considerable advantage of this approach..

Let’s start with scanning our dependencies. We already have a Snyk account, and our API key is stored as a secret called SNYK_TOKEN. So, instead of creating an extra step in the build job, create a new job called opensource-security.

1opensource-security:
2   runs-on: ubuntu-latest
3   steps:
4     - uses: actions/checkout@master
5     - name: Run Snyk to check for vulnerabilities
6       uses: snyk/actions/maven@master
7       env:
8         SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

According to the documentation, the default command is test. Without any additional configuration, it will scan your application for known vulnerabilities in your dependencies. If you want to set a specific CLI argument like --all-projects to accommodate nested projects, use the with keyword and set the property to args.

wordpress-sync/blog-github-actions-properties

While Snyk scans our dependencies, let's check the Java code for vulnerabilities in our GitHub Action. To do this, repeat the previous process by setting up a third job specifically for that, and set the command property to code test.

1 code-security:
2   runs-on: ubuntu-latest
3   steps:
4     - uses: actions/checkout@master
5     - name: Run Snyk to check for vulnerabilities
6       uses: snyk/actions/maven@master
7       env:
8         SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
9       with:
10         command: code test

Notice that all three jobs will run in parallel, which is, in many cases, more efficient.

Adding secure continuous delivery to your GitHub CI/CD

Now, let’s discuss the deployment — or delivery — part of your GitHub CI/CD. After building and testing are completed, it’s time to publish the package to GitHub. To establish this, we’ll use the Maven release plugin in our Java project

Since we only want to release the package when the build job and both security jobs are successfully finished, we’ll add a needs property with a list of prerequisite jobs for release. This way we can ensure that our package isn’t deployed before it’s fully built and secure.

1release:
2   needs: [opensource-security, code-security, build]
3   runs-on: ubuntu-latest
4   steps:
5     - uses: actions/checkout@v2
6     - name: Set up JDK 17
7       uses: actions/setup-java@v1
8       with:
9         java-version: 17
10     - name: Set Git user
11       run: |
12         git config user.email "ghactions@brianvermeer.nl"
13         git config user.name "GitHub Actions"
14     - name: Publish JAR
15       run: mvn -B release:prepare release:perform -DskipTests
16       env:
17         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The maven commands release:prepare and release:perform verify that the release has the right version number and actually gets published to our GitHub repository. Since we’re counting on Maven to handle this, make the following configurations to your pom.xml file.

1  <scm>
2       <developerConnection>scm:git:https://github.com/bmvermeer/ghaction-example.git</developerConnection>
3       <tag>HEAD</tag>
4   </scm>
5   <distributionManagement>
6       <repository>
7           <id>github</id>
8           <name>GitHub</name>
9           <url>https://maven.pkg.github.com/bmvermeer/ghaction-example</url>
10       </repository>
11   </distributionManagement>

Though our newly released application is currently free from vulnerabilities, that doesn’t mean it’ll remain secure forever. New vulnerabilities can show up in a variety of places, making it vital to continually monitor your code and dependencies after an application is deployed.

In addition to scanning during development, we can use Snyk to monitor dependencies post deployment as well. When the release is complete, use the predefined Snyk action once again, but this time set the command property to monitor.

1 opensource-monitor:
2   needs: [release]
3   runs-on: ubuntu-latest
4   steps:
5     - uses: actions/checkout@master
6     - name: Run Snyk to check for vulnerabilities
7       uses: snyk/actions/maven@master
8       env:
9         SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
10       with:
11         command: monitor

This will send the dependency tree, which is static after release, over to Snyk for monitoring. We can now view our project in the Snyk UI, and get automatic notifications if a new vulnerability is found for any of the dependencies we’re using — and ensure that our published Java project remains vulnerability free.

wordpress-sync/blog-github-actions-snyk-ui

Integrating security in your GitHub Actions

With GitHub Actions, creating a CI/CD pipeline for your GitHub project is quite straightforward. And with the Snyk actions, you can easily integrate security scanning on multiple levels for all applications.

GitHub visualizes the pipeline we created today with the following image. With some jobs running in parallel, while others depend on the successful completion of other jobs. You can check out the entire example project in this GitHub repository.

wordpress-sync/blog-github-actions-pipeline-1

Today, we used Snyk Code and Snyk Open Source to scan the Java source code and pipeline dependencies. However, you can expand this by scanning Docker files with Snyk Container and Kubernetes files with Snyk IaC.

With the Snyk GitHub Actions, you can automatically add scanning to your GitHub Actions workflow and mix and match the actions to fit your project best, ensuring code security now and in the future.

Guide to Choosing a SAST Solution

See the process for assessing, selecting, and implementing a modern SAST solution based on a four phase process and find the best fit for your specific security needs.