add support for custom certificates
This commit is contained in:
@@ -12,6 +12,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
|
||||
KAMAL.roles_on(host).each do |role|
|
||||
Kamal::Cli::App::Assets.new(host, role, self).run
|
||||
Kamal::Cli::App::SslCertificates.new(host, role, self).run
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
29
lib/kamal/cli/app/ssl_certificates.rb
Normal file
29
lib/kamal/cli/app/ssl_certificates.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
class Kamal::Cli::App::SslCertificates
|
||||
attr_reader :host, :role, :sshkit
|
||||
delegate :execute, :info, to: :sshkit
|
||||
|
||||
def initialize(host, role, sshkit)
|
||||
@host = host
|
||||
@role = role
|
||||
@sshkit = sshkit
|
||||
end
|
||||
|
||||
def run
|
||||
if role.running_proxy? && role.proxy.custom_ssl_certificate?
|
||||
info "Writing SSL certificates for #{role.name} on #{host}"
|
||||
execute *app.create_ssl_directory
|
||||
if cert_content = role.proxy.certificate_pem_content
|
||||
execute *app.write_certificate_file(cert_content)
|
||||
end
|
||||
if key_content = role.proxy.private_key_pem_content
|
||||
execute *app.write_private_key_file(key_content)
|
||||
end
|
||||
execute *app.set_certificate_permissions
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def app
|
||||
@app ||= KAMAL.app(role: role, host: host)
|
||||
end
|
||||
end
|
||||
@@ -21,6 +21,22 @@ module Kamal::Commands::App::Proxy
|
||||
remove_directory config.proxy_boot.app_directory
|
||||
end
|
||||
|
||||
def create_ssl_directory
|
||||
make_directory(config.proxy_boot.tls_directory)
|
||||
end
|
||||
|
||||
def write_certificate_file(content)
|
||||
[ :sh, "-c", Kamal::Utils.sensitive("cat > #{config.proxy_boot.tls_directory}/cert.pem << 'KAMAL_CERT_EOF'\n#{content}\nKAMAL_CERT_EOF", redaction: "cat > #{config.proxy_boot.tls_directory}/cert.pem << 'KAMAL_CERT_EOF'\n[CERTIFICATE CONTENT REDACTED]\nKAMAL_CERT_EOF") ]
|
||||
end
|
||||
|
||||
def write_private_key_file(content)
|
||||
[ :sh, "-c", Kamal::Utils.sensitive("cat > #{config.proxy_boot.tls_directory}/key.pem << 'KAMAL_KEY_EOF'\n#{content}\nKAMAL_KEY_EOF", redaction: "cat > #{config.proxy_boot.tls_directory}/key.pem << 'KAMAL_KEY_EOF'\n[PRIVATE KEY CONTENT REDACTED]\nKAMAL_KEY_EOF") ]
|
||||
end
|
||||
|
||||
def set_certificate_permissions
|
||||
[ :docker, :exec, "--user", "root", proxy_container_name, "chown", "-R", "kamal-proxy:kamal-proxy", config.proxy_boot.tls_container_directory ]
|
||||
end
|
||||
|
||||
private
|
||||
def proxy_exec(*command)
|
||||
docker :exec, proxy_container_name, "kamal-proxy", *command
|
||||
|
||||
@@ -63,7 +63,7 @@ class Kamal::Configuration
|
||||
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
||||
|
||||
@logging = Logging.new(logging_config: @raw_config.logging)
|
||||
@proxy = Proxy.new(config: self, proxy_config: @raw_config.key?(:proxy) ? @raw_config.proxy : {})
|
||||
@proxy = Proxy.new(config: self, proxy_config: @raw_config.key?(:proxy) ? @raw_config.proxy : {}, secrets: secrets)
|
||||
@proxy_boot = Proxy::Boot.new(config: self)
|
||||
@ssh = Ssh.new(config: self)
|
||||
@sshkit = Sshkit.new(config: self)
|
||||
|
||||
@@ -125,7 +125,8 @@ class Kamal::Configuration::Accessory
|
||||
Kamal::Configuration::Proxy.new \
|
||||
config: config,
|
||||
proxy_config: accessory_config["proxy"],
|
||||
context: "accessories/#{name}/proxy"
|
||||
context: "accessories/#{name}/proxy",
|
||||
secrets: config.secrets
|
||||
end
|
||||
|
||||
def initialize_registry
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
# It is disabled by default on all other roles but can be enabled by setting
|
||||
# `proxy: true` or providing a proxy configuration.
|
||||
proxy:
|
||||
|
||||
# Hosts
|
||||
#
|
||||
# The hosts that will be used to serve the app. The proxy will only route requests
|
||||
@@ -52,6 +51,17 @@ proxy:
|
||||
# Defaults to `false`:
|
||||
ssl: true
|
||||
|
||||
# Custom SSL certificate
|
||||
#
|
||||
# In scenarios where Let's Encrypt is not an option, or you already have your own
|
||||
# certificates from a different Certificate Authority, you can configure kamal-proxy
|
||||
# to load the certificate and the corresponding private key from disk.
|
||||
#
|
||||
# A reference to a secret (in this case, `CERTIFICATE_PEM` and `PRIVATE_KEY_PEM`) will look up the secret
|
||||
# in the local environment:
|
||||
certificate_pem: CERTIFICATE_PEM
|
||||
private_key_pem: PRIVATE_KEY_PEM
|
||||
|
||||
# SSL redirect
|
||||
#
|
||||
# By default, kamal-proxy will redirect all HTTP requests to HTTPS when SSL is enabled.
|
||||
|
||||
@@ -6,11 +6,12 @@ class Kamal::Configuration::Proxy
|
||||
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_reader :config, :proxy_config
|
||||
attr_reader :config, :proxy_config, :secrets
|
||||
|
||||
def initialize(config:, proxy_config:, context: "proxy")
|
||||
def initialize(config:, proxy_config:, secrets:, context: "proxy")
|
||||
@config = config
|
||||
@proxy_config = proxy_config
|
||||
@secrets = secrets
|
||||
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
|
||||
end
|
||||
|
||||
@@ -26,10 +27,36 @@ class Kamal::Configuration::Proxy
|
||||
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
|
||||
end
|
||||
|
||||
def custom_ssl_certificate?
|
||||
proxy_config["certificate_pem"].present? && proxy_config["private_key_pem"].present?
|
||||
end
|
||||
|
||||
def certificate_pem_content
|
||||
secrets[proxy_config["certificate_pem"]]
|
||||
end
|
||||
|
||||
def private_key_pem_content
|
||||
secrets[proxy_config["private_key_pem"]]
|
||||
end
|
||||
|
||||
def certificate_pem
|
||||
tls_file_path("cert.pem")
|
||||
end
|
||||
|
||||
def private_key_pem
|
||||
tls_file_path("key.pem")
|
||||
end
|
||||
|
||||
def tls_file_path(filename)
|
||||
File.join(config.proxy_boot.tls_container_directory, filename) if custom_ssl_certificate?
|
||||
end
|
||||
|
||||
def deploy_options
|
||||
{
|
||||
host: hosts,
|
||||
tls: proxy_config["ssl"].presence,
|
||||
"tls-certificate-path": certificate_pem,
|
||||
"tls-private-key-path": private_key_pem,
|
||||
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
||||
"drain-timeout": seconds_duration(config.drain_timeout),
|
||||
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
||||
@@ -65,7 +92,7 @@ class Kamal::Configuration::Proxy
|
||||
end
|
||||
|
||||
def merge(other)
|
||||
self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config)
|
||||
self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config), secrets: secrets
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -96,6 +96,14 @@ class Kamal::Configuration::Proxy::Boot
|
||||
File.join app_container_directory, "error_pages"
|
||||
end
|
||||
|
||||
def tls_directory
|
||||
File.join app_directory, "tls"
|
||||
end
|
||||
|
||||
def tls_container_directory
|
||||
File.join app_container_directory, "tls"
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_valid_bind_ips(bind_ips)
|
||||
bind_ips.present? && bind_ips.each do |ip|
|
||||
|
||||
@@ -150,8 +150,8 @@ class Kamal::Configuration::Role
|
||||
end
|
||||
|
||||
def ensure_one_host_for_ssl
|
||||
if running_proxy? && proxy.ssl? && hosts.size > 1
|
||||
raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}"
|
||||
if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?
|
||||
raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -173,6 +173,7 @@ class Kamal::Configuration::Role
|
||||
@specialized_proxy = Kamal::Configuration::Proxy.new \
|
||||
config: config,
|
||||
proxy_config: proxy_config,
|
||||
secrets: config.secrets,
|
||||
context: "servers/#{name}/proxy"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,14 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
|
||||
if (config.keys & [ "host", "hosts" ]).size > 1
|
||||
error "Specify one of 'host' or 'hosts', not both"
|
||||
end
|
||||
|
||||
if config["certificate_pem"].present? && config["private_key_pem"].blank?
|
||||
error "Missing private_key_pem setting (required when certificate_pem is present)"
|
||||
end
|
||||
|
||||
if config["private_key_pem"].present? && config["certificate_pem"].blank?
|
||||
error "Missing certificate_pem setting (required when private_key_pem is present)"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user