Azure Key Vault

Build Status Jenkins Plugin GitHub release Jenkins Plugin Installs

This plugin enables Jenkins to fetch secrets from Azure Key Vault and inject them directly into build jobs. It works similarly to the Credential Binding Plugin and borrows much from the Hashicorp Vault Plugin. The plugin acts as an Azure Active Directory Application and must be configured with a valid credential.

System Configuration

Via UI

In the Jenkins Configure System page, configure the following two options in the Azure Key Vault Plugin section

  • Key Vault URL - The url where your Key Vault resides (e.g. https://myvault.vault.azure.net/)
  • Credential ID - The ID associated with a secret in the Jenkins secret store. Supported types are:
    • Azure Service Principal
    • Azure Managed Identity (both user and system assigned)

Via configuration-as-code

This plugin supports the configuration as code plugin:

Example yaml:

credentials:
  system:
    domainCredentials:
      - credentials:
        - azure:
            azureEnvironmentName: "Azure"
            clientId: "d63d9de6-5f7a-48c1-ac1d-e90d4f5e5dcc"
            clientSecret: "${CLIENT_SECRET}"
            description: "An azure service principal"
            id: "service-principal"
            scope: SYSTEM
            subscriptionId: "d63d9de6-5f7a-48c1-ac1d-e90d4f5e5dcc"
            tenant: "d63d9de6-5f7a-48c1-ac1d-e90d4f5e5dcc"

unclassified:
  azureKeyVault:
    keyVaultURL: https://not-a-real-vault.vault.azure.net
    credentialID: service-principal

Via system properties

URL:

-Djenkins.azure-keyvault.url=https://my.vault.azure.net

User or System Assigned Managed Identity:

-Djenkins.azure-keyvault.uami.enabled=true
-Djenkins.azure-keyvault.sp.scope=(GLOBAL|SYSTEM) # defaults to global

Service principal:

-Djenkins.azure-keyvault.sp.client_id=...
-Djenkins.azure-keyvault.sp.client_secret=...
-Djenkins.azure-keyvault.sp.subscription_id=...
-Djenkins.azure-keyvault.sp.tenant_id=...
-Djenkins.azure-keyvault.sp.scope=(GLOBAL|SYSTEM) # defaults to global

Via environment variables

URL:

AZURE_KEYVAULT_URL=https://my.vault.azure.net

User or System Assigned Managed Identity:

AZURE_KEYVAULT_UAMI_ENABLED=true
AZURE_KEYVAULT_SP_SCOPE=(GLOBAL|SYSTEM) # defaults to global

Service principal:

AZURE_KEYVAULT_SP_CLIENT_ID=...
AZURE_KEYVAULT_SP_CLIENT_SECRET=...
AZURE_KEYVAULT_SP_SUBSCRIPTION_ID=...
AZURE_KEYVAULT_SP_TENANT_ID=...
AZURE_KEYVAULT_SP_SCOPE=(GLOBAL|SYSTEM) # defaults to global

Passing a Secret File

It's possible to pass the secret from a file instead of passing the client secret as is. Simply provide the path to the secret file.

-Djenkins.azure-keyvault.sp.client_id=...
-Djenkins.azure-keyvault.sp.client_secret_file=/path/to/secret/secretFile
-Djenkins.azure-keyvault.sp.subscription_id=...
-Djenkins.azure-keyvault.sp.tenant_id=...
-Djenkins.azure-keyvault.sp.scope=(GLOBAL|SYSTEM) # defaults to global
AZURE_KEYVAULT_SP_CLIENT_ID=...
AZURE_KEYVAULT_SP_CLIENT_SECRET_FILE=/path/to/secret/secretFile
AZURE_KEYVAULT_SP_SUBSCRIPTION_ID=...
AZURE_KEYVAULT_SP_TENANT_ID=...
AZURE_KEYVAULT_SP_SCOPE=(GLOBAL|SYSTEM) # defaults to global

The plugin will parse the contents of the file as is. The file should only contain the client_secret value.

Building the Plugin

  • Run mvn package, an .hpi file will be generated in the target folder.

Plugin Usage

Usage in Jenkinsfile

Note that the example echos below will only show *****'s as the plugin redacts secrets found in the build log inside the withAzureKeyvault build wrapper.

Scripted

Snippet generator is fully supported for generating the possible values (along with inline help): Go to any pipeline job and click Pipeline Syntax

Or visit the URL: /job/<job-name>/pipeline-syntax/

Simple version:

node {
    def secrets = [
        [ secretType: 'Certificate', name: 'MyCert00', envVariable: 'CERTIFICATE' ],
        [ secretType: 'Secret', name: 'MySecret00', envVariable: 'SECRET' ],
        [ secretType: 'Secret', name: 'MySecret00', version: '342432lkjhdasjld', envVariable: 'SECRET' ]
    ]

    withAzureKeyvault(secrets) {
        sh 'echo $CERTIFICATE'
        sh 'echo $SECRET'
    }
}

With overrides:

static LinkedHashMap<String, Object> secret(String secretName, String envVar) {
  [
    secretType: 'Secret',
    name: secretName,
    version: '342432lkjhdasjld',
    envVariable: envVar
  ]
}

node {
    def secrets = [
        secret('my-secret', 'MY_SECRET')
    ]

    withAzureKeyvault(
            azureKeyVaultSecrets: secrets,
            keyVaultURLOverride: 'https://mykeyvault.vault.azure.net',
            credentialIDOverride: 'service-principal'
    ) {
        sh 'echo $MY_SECRET'
     }
}

Declarative

Snippet generator is fully supported for generating the possible values (along with inline help): Go to any pipeline job and click Pipeline Syntax -> Declarative Directive Generator

Or visit the URL: /job/<job-name>/directive-generator/

Simple:

pipeline {
    agent any
    stages {
        stage('Build') {
            options {
              azureKeyVault([[envVariable: 'MY_SECRET', name: 'my-secret', secretType: 'Secret']])
            }
            steps {
                sh "echo $SECRET"
            }
        }
    }
}

With overrides:

pipeline {
    agent any
    stages {
        stage('Build') {
            options {
              azureKeyVault(
                credentialID: 'my-sp',
                keyVaultURL: 'https://my.vault.azure.net',
                secrets: [
                    [envVariable: 'MY_SECRET', name: 'my-secret', secretType: 'Secret']
                ]
              )
            }
            steps {
                sh "echo $SECRET"
            }
        }
    }
}

Certificate:

pipeline {
    agent any
    stages {
        stage('Build') {
            options {
              azureKeyVault([[envVariable: 'CERT_LOCATION', name: 'my-cert-name', secretType: 'Certificate']])
            }
            steps {
                sh "openssl pkcs12 -in $CERT_LOCATION  -nodes -password 'pass:' -out keyvault.pem"
            }
        }
    }
}

The shell command above will convert the PFX file to a pem key file (also containing the certificate), note that Azure Key Vault removes the password on the pfx when you import it, if you're importing it back into Azure somewhere else you may need to convert it to pem and convert back to a pfx with a password.

Azure Key Vault Credentials Provider

Note: It is not supported to configure the credential provider with the Configuration as Code plugin and resolving credentials from Azure Key Vault in the same configuration file. Please use one of the other options (system properties, environment variables) if you want to retrieve secrets for use in Configuration as Code files

This plugin enables the retrieval of Secrets directly from Azure Key Vault. After the configuration is set up, secrets from the key vault can be viewed in the credentials page like this:

provider

Note These credentials are read-only and metadata caching(10 minutes) means newly created secrets may not be here immediately. You can reload the cache on the system configuration page if you need a new secret to appear.

Use these credentials just as other normal credentials in Jenkins.

There are multiple supported credential types, string is used by default. To use a different type add a tag called type with one of the below values:

  • string - Secret text
  • username - Username with password
    • add a tag username for the username of the credential
  • secretFile - a file with secret content
    • (optional) add a tag fileName for the secret file name, when it is fetched. Default is ${secretNameIntheVault}.txt.
  • sshUserPrivateKey - SSH Private key
    • add a tag username for the username of the credential
    • (optional) add a tag username-is-secret and set it to true to hide the username in the build logs
    • (optional) add a tag passphrase-id that points to the secret name in the vault that has the passphrase that should be used with the ssh keys
  • certificate - a certificate as secret
    • (optional) add tag password-id that points to the secret name in the vault that has the password of the certificate.
  • githubApp - a GitHub App credential (requires the github-branch-source plugin to be installed)
    • add a tag clientID for the GitHub App client ID (required)
    • (optional) add a tag owner for the organisation/user the app is scoped to (leave unset if the app is installed on a single organisation)
    • (optional) add a tag apiUri for the GitHub API endpoint (GitHub Enterprise only)

Secret String

Declarative Pipeline:

pipeline {
    agent any
    environment {
        GITHUB_API_TOKEN = credentials('github-api-token')
    }
    stages {
        stage('Foo') {
            steps {
                echo '$GITHUB_API_TOKEN'
            }
        }
    }
}

Scripted Pipeline:

node {
    withCredentials([string(credentialsId: 'github-api-token', variable: 'GITHUB_API_TOKEN')]) {
        echo '$GITHUB_API_TOKEN'
    }
}

Username with password

az keyvault secret set --vault-name my-vault \
  --name github-pat \
  --value my-pat \
  --tags username=github-user type=username

Scripted Pipeline:

job('my example') {
    scm {
        git {
            remote {
                github('my-repo', 'https')
                credentials('github-pat')
            }
        }
    }
}

Secret file

az keyvault secret set --vault-name my-vault \
  --name a-secret-file-vault-secret \
  --value "-----BEGIN test secretFile-----\nline 1\nline2\nbla\nblob" \
  --tags type=secretFile fileName=mySecretFile.txt

Scripted Pipeline:

node {
    withCredentials([
      file(credentialsId: "test-secretFile-credentialsId", variable: "VARIABLE_CONTAINING_PATH_TO_SECRET_FILE")]) {
      sh("doSomething --use-this-secret-file \$VARIABLE_CONTAINING_PATH_TO_SECRET_FILE")
    }
}

SSH Username with private key

az keyvault secret set --tags type=sshUserPrivateKey username=my-username \
  --vault-name my-vault \
  --name test-ssh \
  -f ~/.ssh/my-ssh-key

Scripted pipeline:

# This is a docker image that can be used to test out this feature
docker run --rm -it --publish 2222:2222 \
  -e "PUBLIC_KEY=my-public-key" linuxserver/openssh-server
node {
    withCredentials([sshUserPrivateKey(credentialsId: "test-ssh", keyFileVariable: "my_ssh_key", usernameVariable: "my_username")]) {
     sh 'ssh -i $my_ssh_key -p 2222 $my_username@localhost "uname -r"'
    }
}

Declarative pipeline:

pipeline {
    agent any
    environment {
        SSH_PRIVATE_KEY = credentials('test-ssh')
    }
    stages {
        stage('Foo') {
            steps {
                sh 'ssh -i $SSH_PRIVATE_KEY -p 2222 $SSH_PRIVATE_KEY_USR@localhost "cat world"'
            }
        }
    }
}
SSH Username with private key and passphrase

If your SSH private keys has a passphrase you need to add a tag passphrase-id which references the secret that the passphrase is stored in.

Create the passphrase secret:

az keyvault secret set \
  --vault-name my-vault \
  --name test-ssh-passphrase \
  --value my-ssh-passphrase

Store the SSH key with the passphrase tag:

az keyvault secret set --tags type=sshUserPrivateKey username=my-username passphrase-id=test-ssh-passphrase \
  --vault-name my-vault \
  --name test-ssh \
  -f ~/.ssh/my-ssh-key

If the passphrase can not be found in the vault, the secret will not load and a warning will be logged.

Certificate

It is possible to load certificates from the vault. The certificate needs to be stored in a vault-secret base64-encoded and without whitespace. In addition to the secret containing the keystore info (the certificate), another vault-secret is needed to store the password of the keystore.

  1. If the certificate is protected by a password, Create the password secret:
az keyvault secret set \
  --vault-name my-vault \
  --name secret-containing-keystore-password \
  --value my-keystore-password
  1. Store the (base64-encoded and without whitespace) keystore with the password tag:

if the certificate is protected by a password:

az keyvault secret set \
  --tags type=certificate password-id=secret-containing-keystore-password \
  --vault-name my-vault \
  --name secret-containing-base64encoded-keystore \
  --value {base64-encoded-keystore}

if the certificate has no password ( a zero length password )

az keyvault secret set \
  --tags type=certificate \
  --vault-name my-vault \
  --name secret-containing-base64encoded-keystore \
  --value {base64-encoded-keystore}

If the tag password-id is set but the password can not be found in the vault, the secret will not load and a warning will be logged. If the keystore cannot be decoded, or cannot be loaded the pipeline using the certificate-credentialsId will throw an error.

Scripted pipeline:

node {
    withCredentials([
        certificate(
            credentialsId: certificateCredentialsId,
            keystoreVariable: 'PATH_TO_KEYSTORE',
            passwordVariable: 'PASSWORD'
        )
    ]) {
      sh(
        script: """
            curl -X POST \
                --cert-type P12 \
                --cert "\$PATH_TO_KEYSTORE":\$PASSWORD \
                ${url}
        """
      )
    }
}

GitHub App

It is possible to load GitHub App credentials from the vault. This requires the github-branch-source plugin to be installed; if it is not, secrets tagged type=githubApp are skipped (a warning is logged) and all other credentials still load.

The vault-secret value must be the GitHub App private key in PKCS#8 PEM format. GitHub issues the key in PKCS#1; convert it once with:

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \
  -in downloaded-github-app-key.pem \
  -out github-app-key.pkcs8.pem

Store the converted key, tagging it with the client ID (and, optionally, the owner / API endpoint):

az keyvault secret set \
  --tags type=githubApp clientID=Iv23liExample owner=my-org \
  --vault-name my-vault \
  --name my-github-app \
  --file github-app-key.pkcs8.pem

For GitHub Enterprise, also add apiUri=https://ghe.example.com/api/v3 to the tags.

The credential is exposed as a native GitHubAppCredentials, so it can be selected anywhere a GitHub App credential is accepted (e.g. GitHub Branch Source, the checkout/git steps). Note that, unlike the other types, the private key is read from the vault when credentials are refreshed rather than lazily on use, because GitHubAppCredentials reads its key eagerly during token generation.

Secret Labels

You can filter which secrets are visible to the credentials provider. By default, the plugin will load all secrets stored within the Key Vault. However, your Key Vault may be the Secret Source for multiple applications, or contains secrets not needed directly by Jenkins. To filter out secrets from being set, add a System Property or Environment Variable:

Via System Property:

-Djenkins.azure-keyvault.label-selector=myCustomLabel

Via Environment Variable:

AZURE_KEYVAULT_LABEL_SELECTOR=myCustomLabel

If included in your config, when the Azure Key Vault plugin is resolving credentials from your Key Vault, it will skip any secret that does not contain a tag jenkins-label=myCustomLabel. For example, if two secrets are set within the KeyVault:

az keyvault secret set --vault-name my-vault \
  --name testUserNoLabel \
  --value example1 \
  --tags username=testUserNoLabel type=username
az keyvault secret set --vault-name my-vault \
  --name testUserWithLabel \
  --value example2 \
  --tags username=testUserWithLabel type=username jenkins-label=myCustomLabel

Multiple label selectors can be specified as a comma separated list:

AZURE_KEYVAULT_LABEL_SELECTOR=myCustomLabel,anotherCustomLabel
az keyvault secret set --vault-name my-vault \
  --name testUserWithLabel \
  --value example2 \
  --tags jenkins-label=myCustomLabel1,myCustomLabel2

With the System Property or Environment variable being set in this example, only the usernamePassword testUserWithLabel will be present in your Jenkins instance.

Jenkins Credential ID, Description and Scope

The ID, description and scope of credentials can be specified with tags on the Azure Key Vault secret. The ID is specified with the tag "jenkinsID" and appears in the Jenkins credentials UI in the "ID" column. This is the credentials ID parameter used in withCredentials() calls. The description is specified with the tag "description" and appears in the Jenkins credentials UI in the "Name" column. The scope is specified with the tag "scope" and can be set to "system" or "global". By default the scope will be set to global.

az keyvault secret set --vault-name my-vault \
  --name testUserWithLabel \
  --value example2 \
  --tags jenkinsID=myCred description="This is my credential" scope=system

Folder scoped credentials

A Key Vault can also be configured on a folder (including multibranch projects and organization folders), which requires the Folders plugin. The secrets in that vault are then available only to items inside the folder and its sub-folders, allowing teams to use their own vault without it being visible to the whole Jenkins instance.

On the folder's configuration page, add the Azure Key Vault property and set the Key Vault URL and the credential used to connect to it. The credential is looked up from the folder, so it can be an Azure Service Principal or Managed Identity stored in the folder itself, in a parent folder or globally.

Secrets are mapped to credentials in exactly the same way as for the global vault, using the tags described above. When the same credential ID is available from more than one folder, the one in the nearest folder is used. Secrets with scope=system are ignored in folders, as system credentials are only usable by Jenkins itself.

The credentials of a folder vault are cached in the same way as the global vault. Use Reload cache on the folder configuration page, or save the folder configuration, to pick up new secrets immediately.

With Job DSL:

folder('team-a') {
    properties {
        azureKeyVault {
            url('https://team-a.vault.azure.net/')
            credentialID('team-a-service-principal')
        }
    }
}

Adding credential types from other plugins

Each type tag value is handled by an AzureKeyVaultCredentialsAdapter extension, so other plugins can add their own credential types without changing this plugin:

@Extension
public class MyCredentialsAdapter extends AzureKeyVaultCredentialsAdapter {
    @NonNull
    @Override
    public String getType() {
        return "myType";
    }

    @Override
    public IdCredentials adapt(@NonNull AzureKeyVaultSecretContext context) {
        return new MyAzureCredentials(
                context.getScope(), context.getId(), context.getDescription(),
                context.getTag("someTag"), context.getSecret());
    }
}

If the plugin providing the credential type is an optional dependency, register the adapter with @OptionalExtension(requirePlugins = "...") from the variant plugin instead.

context.getSecret() returns a supplier that reads the secret from the vault on every get(); context.getSecret(name) does the same for another secret in the same vault (for example a passphrase). A credential class backed by it should:

  • Keep the supplier rather than its value, so the secret is only read from the vault when the credential is used.
  • Have a CredentialsSnapshotTaker that captures the value, so the credential can be sent to agents. See the *SnapshotTaker classes in this plugin.
  • Use a descriptor extending AzureKeyVaultCredentialsDescriptor, which makes the type available only to the Azure Key Vault credentials providers.

SecretSource

The plugin allows the Configuration as Code plugin to interpolate string secrets from Azure KeyVault.

Example

az cli:

az keyvault secret set --vault-name my-vault --name my-password --value password

JCasC:

jenkins:
  securityRealm:
    local:
      allowsSignup: false
      users:
      - id: "foo"
        password: "${my-password}"