From e516f427cd0b3d4b3b7dc54296ba5cbbdce64beb Mon Sep 17 00:00:00 2001 From: Nathan Anderson Date: Wed, 18 Jan 2023 17:35:36 -0500 Subject: [PATCH 1/6] Enable docker secrets in the builder as a more secure alternative to build args. --- README.md | 26 +++++++++++++++++++++++++- lib/mrsk/commands/builder/base.rb | 9 +++++++++ lib/mrsk/configuration.rb | 6 +++++- test/commands/builder_test.rb | 5 +++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75283fc0..7dce93ce 100644 --- a/README.md +++ b/README.md @@ -146,9 +146,33 @@ builder: multiarch: false ``` +### Configuring build secrets for new images + +Some images might need an secret passed in during build time, like a GITHUB_TOKEN to give access to private gem repositories, but you don't want it exposed in the resulting image. This can be done like so: + +```yaml +builder: + secrets: + - GITHUB_TOKEN +``` + +This build secret can then be used in the Dockerfile: + +``` +# Install application gems +COPY Gemfile Gemfile.lock ./ + +# Private repositories need an access token during the build +RUN --mount=type=secret,id=GITHUB_TOKEN \ + BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \ + bundle install +``` + +> Note: This only supports simple secret configurations, and not the full gamut of options presented by the [buildx command --secret option](https://docs.docker.com/engine/reference/commandline/buildx_build/#secret). + ### Configuring build args for new images -Some images might need an argument passed in during build time, like a GITHUB_TOKEN to give access to private gem repositories. This can be done like so: +Some images might need an argument passed in during build time, like a GITHUB_TOKEN to give access to private gem repositories. This is less secure than a docker secret, but works on older versions of docker. This will also expose your value in the final image. This can be done like so: ```yaml builder: diff --git a/lib/mrsk/commands/builder/base.rb b/lib/mrsk/commands/builder/base.rb index 54446dec..77477341 100644 --- a/lib/mrsk/commands/builder/base.rb +++ b/lib/mrsk/commands/builder/base.rb @@ -2,6 +2,7 @@ require "mrsk/commands/base" class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base delegate :argumentize, to: Mrsk::Configuration + delegate :simple_secretize, to: Mrsk::Configuration def pull docker :pull, config.absolute_image @@ -11,8 +12,16 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base argumentize "--build-arg", args, redacted: true end + def build_secrets + simple_secretize "--secret", secrets, redacted: true + end + private def args config.builder["args"] || {} end + + def secrets + config.builder["secrets"] || {} + end end diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index adb9c054..1b8dd1f1 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -22,6 +22,10 @@ class Mrsk::Configuration attributes.flat_map { |k, v| [ argument, redacted ? Mrsk::Utils.redact("#{k}=#{v}") : "#{k}=#{v}" ] } end + def simple_secretize(secret, attributes, redacted: false) + attributes.flat_map { |k, v| [ secret, redacted ? Mrsk::Utils.redact("id=#{k}") : "id=#{k}" ] } + end + private def load_config_file(file) if file.exist? @@ -137,7 +141,7 @@ class Mrsk::Configuration if config.registry["username"].blank? raise ArgumentError, "You must specify a username for the registry in config/deploy.yml" - end + end if config.registry["password"].blank? raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)" diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index d8c58a0f..444e674f 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -27,6 +27,11 @@ class CommandsBuilderTest < ActiveSupport::TestCase assert_equal [ "--build-arg", "a=1", "--build-arg", "b=2" ], builder.target.build_args end + test "build secrets" do + builder = Mrsk::Commands::Builder.new(Mrsk::Configuration.new(@config.merge({ builder: { "secrets" => ["token_a", "token_b"] } }))) + assert_equal [ "--secret", "id=token_a", "--secret", "id=token_b" ], builder.target.build_secrets + end + test "native push with build args" do builder = Mrsk::Commands::Builder.new(Mrsk::Configuration.new(@config.merge({ builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } } }))) assert_equal [ :docker, :build, "-t", "--build-arg", "a=1", "--build-arg", "b=2", "dhh/app:123", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push From c16d95013610ad6dd9d8dfa32eb4e9c6fcd563cb Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Jan 2023 10:04:34 +0100 Subject: [PATCH 2/6] Refine docs on build secrets --- README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7dce93ce..3c5cc4f4 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ builder: ### Configuring build secrets for new images -Some images might need an secret passed in during build time, like a GITHUB_TOKEN to give access to private gem repositories, but you don't want it exposed in the resulting image. This can be done like so: +Some images need a secret passed in during build time, like a GITHUB_TOKEN to give access to private gem repositories. This can be done by having the secret in ENV, then referencing it like so in the configuration: ```yaml builder: @@ -168,28 +168,22 @@ RUN --mount=type=secret,id=GITHUB_TOKEN \ bundle install ``` -> Note: This only supports simple secret configurations, and not the full gamut of options presented by the [buildx command --secret option](https://docs.docker.com/engine/reference/commandline/buildx_build/#secret). - ### Configuring build args for new images -Some images might need an argument passed in during build time, like a GITHUB_TOKEN to give access to private gem repositories. This is less secure than a docker secret, but works on older versions of docker. This will also expose your value in the final image. This can be done like so: +Build arguments that aren't secret can be configured like so: ```yaml builder: args: - GITHUB_TOKEN: <%= ENV["GITHUB_TOKEN"] %> + RUBY_VERSION: 3.2.0 ``` -This build arg can then be used in the Dockerfile: +This build argument can then be used in the Dockerfile: ``` # Private repositories need an access token during the build -ARG GITHUB_TOKEN -ENV BUNDLE_GITHUB__COM=x-access-token:$GITHUB_TOKEN - -# Install application gems -COPY Gemfile Gemfile.lock ./ -RUN bundle install +ARG RUBY_VERSION +FROM ruby:$RUBY_VERSION-slim as base ``` ## Commands From a8779f705527ffab10bc4c1a1bbf8802f3b8e97d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Jan 2023 10:07:17 +0100 Subject: [PATCH 3/6] Simpler API No need for redactions, since values aren't shared. --- lib/mrsk/commands/builder/base.rb | 5 ++--- lib/mrsk/configuration.rb | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/mrsk/commands/builder/base.rb b/lib/mrsk/commands/builder/base.rb index 77477341..8309d7e5 100644 --- a/lib/mrsk/commands/builder/base.rb +++ b/lib/mrsk/commands/builder/base.rb @@ -1,8 +1,7 @@ require "mrsk/commands/base" class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base - delegate :argumentize, to: Mrsk::Configuration - delegate :simple_secretize, to: Mrsk::Configuration + delegate :argumentize, :secretize, to: Mrsk::Configuration def pull docker :pull, config.absolute_image @@ -13,7 +12,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base end def build_secrets - simple_secretize "--secret", secrets, redacted: true + secretize "--secret", secrets end private diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index 1b8dd1f1..c626013f 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -22,8 +22,8 @@ class Mrsk::Configuration attributes.flat_map { |k, v| [ argument, redacted ? Mrsk::Utils.redact("#{k}=#{v}") : "#{k}=#{v}" ] } end - def simple_secretize(secret, attributes, redacted: false) - attributes.flat_map { |k, v| [ secret, redacted ? Mrsk::Utils.redact("id=#{k}") : "id=#{k}" ] } + def secretize(secret, attributes) + attributes.flat_map { |k, v| [ secret, "id=#{k}" ] } end private From 52fe8d358e37219215867696da8add26b177e8fe Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Jan 2023 10:13:03 +0100 Subject: [PATCH 4/6] Secrets come just as keys --- lib/mrsk/commands/builder/base.rb | 2 +- lib/mrsk/configuration.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mrsk/commands/builder/base.rb b/lib/mrsk/commands/builder/base.rb index 8309d7e5..237d06ca 100644 --- a/lib/mrsk/commands/builder/base.rb +++ b/lib/mrsk/commands/builder/base.rb @@ -21,6 +21,6 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base end def secrets - config.builder["secrets"] || {} + config.builder["secrets"] || [] end end diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index c626013f..5efa5dac 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -22,8 +22,8 @@ class Mrsk::Configuration attributes.flat_map { |k, v| [ argument, redacted ? Mrsk::Utils.redact("#{k}=#{v}") : "#{k}=#{v}" ] } end - def secretize(secret, attributes) - attributes.flat_map { |k, v| [ secret, "id=#{k}" ] } + def secretize(secret, keys) + keys.flat_map { |key| [ secret, "id=#{key}" ] } end private From 454015b294839735e06c47b9e20bebaef6551d1e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Jan 2023 10:24:23 +0100 Subject: [PATCH 5/6] Reuse argumentize for build secrets --- lib/mrsk/commands/builder/base.rb | 3 ++- lib/mrsk/configuration.rb | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/mrsk/commands/builder/base.rb b/lib/mrsk/commands/builder/base.rb index 237d06ca..a811afbd 100644 --- a/lib/mrsk/commands/builder/base.rb +++ b/lib/mrsk/commands/builder/base.rb @@ -2,6 +2,7 @@ require "mrsk/commands/base" class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base delegate :argumentize, :secretize, to: Mrsk::Configuration + delegate :secretize, to: Mrsk::Utils def pull docker :pull, config.absolute_image @@ -12,7 +13,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base end def build_secrets - secretize "--secret", secrets + argumentize "--secret", secrets.collect { |secret| [ "id", secret ] } end private diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index 5efa5dac..ad5b1225 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -22,10 +22,6 @@ class Mrsk::Configuration attributes.flat_map { |k, v| [ argument, redacted ? Mrsk::Utils.redact("#{k}=#{v}") : "#{k}=#{v}" ] } end - def secretize(secret, keys) - keys.flat_map { |key| [ secret, "id=#{key}" ] } - end - private def load_config_file(file) if file.exist? From 9bb1fb71660c847417d8d7f9555280234cac1af1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 20 Jan 2023 10:26:36 +0100 Subject: [PATCH 6/6] Move argumentize to Utils --- lib/mrsk/commands/builder/base.rb | 3 +-- lib/mrsk/configuration.rb | 7 ++----- lib/mrsk/configuration/role.rb | 2 +- lib/mrsk/utils.rb | 5 +++++ 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/mrsk/commands/builder/base.rb b/lib/mrsk/commands/builder/base.rb index a811afbd..d08e8a87 100644 --- a/lib/mrsk/commands/builder/base.rb +++ b/lib/mrsk/commands/builder/base.rb @@ -1,8 +1,7 @@ require "mrsk/commands/base" class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base - delegate :argumentize, :secretize, to: Mrsk::Configuration - delegate :secretize, to: Mrsk::Utils + delegate :argumentize, to: Mrsk::Utils def pull docker :pull, config.absolute_image diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index ad5b1225..0a097480 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -7,6 +7,7 @@ require "mrsk/utils" class Mrsk::Configuration delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :config, allow_nil: true + delegate :argumentize, to: Mrsk::Utils class << self def create_from(base_config_file, destination: nil) @@ -18,10 +19,6 @@ class Mrsk::Configuration end) end - def argumentize(argument, attributes, redacted: false) - attributes.flat_map { |k, v| [ argument, redacted ? Mrsk::Utils.redact("#{k}=#{v}") : "#{k}=#{v}" ] } - end - private def load_config_file(file) if file.exist? @@ -94,7 +91,7 @@ class Mrsk::Configuration def env_args if config.env.present? - self.class.argumentize "-e", config.env + argumentize "-e", config.env else [] end diff --git a/lib/mrsk/configuration/role.rb b/lib/mrsk/configuration/role.rb index 9051a571..6372372f 100644 --- a/lib/mrsk/configuration/role.rb +++ b/lib/mrsk/configuration/role.rb @@ -1,5 +1,5 @@ class Mrsk::Configuration::Role - delegate :argumentize, to: Mrsk::Configuration + delegate :argumentize, to: Mrsk::Utils attr_accessor :name diff --git a/lib/mrsk/utils.rb b/lib/mrsk/utils.rb index 75e2f226..10d8323b 100644 --- a/lib/mrsk/utils.rb +++ b/lib/mrsk/utils.rb @@ -1,6 +1,11 @@ module Mrsk::Utils extend self + # 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}" ] } + end + # Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes def redact(arg) # Used in execute_command to hide redact() args a user passes in arg.to_s.extend(SSHKit::Redaction) # to_s due to our inability to extend Integer, etc