jenkinsci / hashicorp-vault-plugin

Jenkins plugin to populate environment variables from secrets stored in HashiCorp's Vault.
https://plugins.jenkins.io/hashicorp-vault-plugin/
MIT License
218 stars 144 forks source link

Support hudson.util.Secret for JCasC SecretSource variables #169

Open samrocketman opened 3 years ago

samrocketman commented 3 years ago

For new instances it makes sense to have plain text environment variables.

However a long running modern production Jenkins instance might have autoscaling and a long lived config through jenkins config as code (JCasC).

Feature Request

For AppRole auth and other JCASC environment variables, supporting encrypted hudson.util.Secret strings is desirable.

This could be implemented in VaultSecretSource.getVariable() method.

samrocketman commented 3 years ago

Related to #170.

Better bootstrap of long running Jenkins instances is my line of thinking.

jetersen commented 3 years ago

Not sure how feasible this is without support coming from JCasC

https://github.com/jenkinsci/configuration-as-code-plugin/issues/1141

samrocketman commented 3 years ago

Does not require JCasC support

All of the code and implementation would exist in this repository.

Example implementation

hudson.util.Secret is a core Jenkins API. You could edit the following code.

https://github.com/jenkinsci/hashicorp-vault-plugin/blob/2e2760b322ee470e638437187616f46b0a89fadf/src/main/java/com/datapipe/jenkins/vault/jcasc/secrets/VaultSecretSource.java#L226-L228

Here's an example implementation which will probably work as intended. Generally, I recommend import hudson.util.Secret; but for brevity I skip that in the following example to reference it directly.

private Optional<String> getVariable(String key) { 
    Optional<String> value = Optional.ofNullable(prop.getProperty(key, System.getenv(key)));
    if(value != null) {
        value = Optional.ofNullable(hudson.util.Secret.fromString(value.toString()).getPlainText());
    }
    return value;
} 

The intent being you could set an environment variable like the following.

export CASC_VAULT_APPROLE='{ABC1234AAAAAQ1/JHKggxIlBcuVqegoa2AdyVaNvjWIFk430/vI4jEBM=}'
export CASC_VAULT_APPROLE_SECRET='{DEF5678AAAAAQ1/JHKggxIlBcuVqegoa2AdyVaNvjWIFk430/vI4jEBM=}'

This implementation would enable CASC_VAULT_FILE vault properties file to also support encrypted secrets in its file so you're not storing secrets as plain text on disk.

Reasoning

This is especially important in backups. I store the Jenkins secret keys ($JENKINS_HOME/secret*) separately from the Jenkins backup so that Jenkins backup config is secured (encrypted at rest).

In a long-lived production configuration, an admin could run hudson.util.Secret.fromString('plain text').getEncryptedValue() in the script console to get the desired encrypted string. This would be typical for a migration from an existing Jenkins instance to using JCasC.

samrocketman commented 3 years ago

How I would bootstrap new environments

I would have a $JENKINS_HOME/init.groov.d/init-jcasc.groovy initialization script which would do the following.

  1. In AWS, there would be an instance role to AWS Secrets manager granting read access as well as read access to an S3 bucket containing jenkins.yaml jcasc file for dev/staging/prod blue/green.
  2. If $JENKINS_HOME/vault-secretsource.properties file does not exist; create it pulling properties from AWS secrets manager.
  3. If $JENKINS_HOME/jenkins.yaml file does not exist; copy it from S3. (use SHA256 hashing to verify it is up to date or update it).
  4. Reload JCasC via Java APIs. I verified through code review that a configuration reload would re-intialize all Secret sources including VaultSecretSource.
samrocketman commented 3 years ago

What are your thoughts on my proposed solution? I could open a PR but I might need help with mocking/testing as that's an area where I'm weak.

I could compile it and run it on a live Jenkins instance manually to verify it works.