From f6ca864e06882d1a0715e6723168c1e06865c694 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 21 Jan 2023 10:56:24 +0100 Subject: [PATCH] Add secret envs --- README.md | 19 +++++++++++++++++++ lib/mrsk/configuration.rb | 10 +++++++++- lib/mrsk/utils.rb | 2 +- test/configuration_test.rb | 24 ++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df6e034c..a7caf0e2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,25 @@ env: REDIS_URL: redis://redis1:6379/1 ``` +### Adding secret custom env variables + +If you have custom env variables that are secret, you can divide the `env` block into `clear` and `secret`: + +```yaml +env: + clear: + DATABASE_URL: mysql2://db1/hey_production/ + REDIS_URL: redis://redis1:6379/1 + secret: + - DATABASE_PASSWORD + - REDIS_PASSWORD +``` + +The list of secret env variables will be expanded at run time from your local machine. So a reference to a secret `DATABASE_PASSWORD` will look for `ENV["DATABASE_PASSWORD"]` on the machine running MRSK. Just like with build secrets. + +Note: Marking an ENV as secret currently only redacts its value in the output for MRSK. The ENV is still injected in the clear into the container at runtime. + + ### Splitting servers into different roles If your application uses separate hosts for running jobs or other roles beyond the default web running, you can specify these hosts and their custom entrypoint command like so: diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index 914948f1..e58cd0fc 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -81,7 +81,11 @@ class Mrsk::Configuration def env_args if config.env.present? - argumentize "-e", config.env + if config.env["secret"].present? + argumentize("-e", expand_env_secrets, redacted: true) + argumentize("-e", config.env["clear"]) + else + argumentize "-e", config.env + end else [] end @@ -134,6 +138,10 @@ class Mrsk::Configuration def role_names config.servers.is_a?(Array) ? [ "web" ] : config.servers.keys.sort end + + def expand_env_secrets + config.env["secret"].to_h { |key| [ key, ENV[key] ] } + end end require "mrsk/configuration/role" diff --git a/lib/mrsk/utils.rb b/lib/mrsk/utils.rb index 10d8323b..01462fbf 100644 --- a/lib/mrsk/utils.rb +++ b/lib/mrsk/utils.rb @@ -3,7 +3,7 @@ module Mrsk::Utils # Return a list of shell arguments using the same named argument against the passed attributes. def argumentize(argument, attributes, redacted: false) - attributes.flat_map { |k, v| [ argument, redacted ? redact("#{k}=#{v}") : "#{k}=#{v}" ] } + Array(attributes).flat_map { |k, v| [ argument, redacted ? redact("#{k}=#{v}") : "#{k}=#{v}" ] } end # Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 71fb3061..0e7e523a 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -89,6 +89,30 @@ class ConfigurationTest < ActiveSupport::TestCase assert_equal [ "-e", "REDIS_URL=redis://x/y" ], @config.env_args end + test "env args with clear and secrets" do + ENV["PASSWORD"] = "secret123" + config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({ + env: { "clear" => { "PORT" => "3000" }, "secret" => [ "PASSWORD" ] } + }) }) + + assert_equal [ "-e", "PASSWORD=secret123", "-e", "PORT=3000" ], config.env_args + assert config.env_args[1].is_a?(SSHKit::Redaction) + ensure + ENV["PASSWORD"] = nil + end + + test "env args with only secrets" do + ENV["PASSWORD"] = "secret123" + config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({ + env: { "secret" => [ "PASSWORD" ] } + }) }) + + assert_equal [ "-e", "PASSWORD=secret123" ], config.env_args + assert config.env_args[1].is_a?(SSHKit::Redaction) + ensure + ENV["PASSWORD"] = nil + end + test "ssh options" do assert_equal "root", @config.ssh_options[:user]