Azure DevOps Pipelines – how secret are secret variables?

Azure Pipelines supports storing secret variables within the project, either through variable groups or as secret variables.

This is a convenient place to store all those database connection strings and access tokens you need to allow access to external services like JFrog Artifactory or deploy to Azure services such as a Kubernetes cluster or CosmosDB.

Simply enter the secret value, check the padlock and everything is safe right? Well depends what you mean by ‘safe’!

Not so fast kiddo

Although Azure Pipelines take pains to obscure the echoing of secrets to the pipeline console and even prevents secret being made available by default to pipeline scripts this does not mean the secrets can’t be still be , how shall we say, obtained.

A pipeline developer who has access to the azure-pipelines.yml can quite easily grab the secret value, echo it to a file, publish that file as a pipeline artifact and then simply download the file from the pipeline console once run.

What do you mean – ‘developers can see the production passwords’?

Well, if you use variable groups to store secret variables for each environment you deploy to, then YES!

Now this may be fine if only admins have access to run the pipelines, but if your azure-pipelines.yml file is embedded within your application source code this in theory means an application developer could change the pipeline definition to reveal production secrets.

So how do I prevent this happening in our team?

Luckily the Azure Pipelines security onion does have a good selection of layers to peel back.

  1. Force azure-pipelines.yml to extend a ‘master’ template which restricts what tasks and scripts can be run by the child script using a mechanism similar to inheritance. Use a ‘required template’ check to ensure only sanctioned templates that extend ‘master’ can be run.
  2. Don’t put your azure-pipelines.yml in your app source code, instead store it within separate protected repo and pull in a reference to your app repo.
  3. Use permissions on variable groups to allow access to pipeline admin level roles excluding developer roles. However this only works in devs are not allowed to deploy code to higher environments.
  4. Separate the CI phase from the CD phase. This is similar to the previous technique and is in fact how pre yaml Azure Pipelines structures it’s builds. You could argue that the CI phase can be run freely by developers including deploying to a dev environment but the CD phase should only be accessible more privileged user that can promote the deployment through the various environments to production.
  5. Finally, don’t use pipeline library secret variables. Instead use Service Connections, but a problem here is that not all services support service connections e.g. Azure Databricks.

Security should always be concern number zero with any production system and a CD Pipeline that holds the keys to so many precious castles is a core component to protect.

Spring Boot Microservice Integration Test Using Hoverfly

  • WebTestClient used to simplify external web service calls into microservice
  • SpringBootTest enabled with WebEnvironment defined port to enable webserver
  • Hoverfly to mock external webservice and provide precanned responses
package hello;

import io.specto.hoverfly.junit.core.HoverflyConfig;
import io.specto.hoverfly.junit.rule.HoverflyRule;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;

import static io.specto.hoverfly.junit.core.SimulationSource.defaultPath;

@RunWith(SpringRunner.class)
@SpringBootTest(
		webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
		classes = hello.Application.class)
public class IntegrationTests {

	@ClassRule
	public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(defaultPath("sm9-create-ticket.json"),
			HoverflyConfig.localConfigs().asWebServer().proxyPort(8500));

	@Autowired
	private WebTestClient webClient;

	@Test
	public void testCreateTicket() {

		this.webClient.get().uri("/ticket?query=x").exchange().expectStatus().isOk()
				.expectBody(String.class).isEqualTo("Response from HPSM for create ticket");

	}

}