Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/kamal/configuration/docs/proxy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ proxy:
# Defaults to `false`:
ssl: true

# On-demand TLS
#
# For apps serving customer-supplied domains, which aren't known at deploy time.
# kamal-proxy asks the given endpoint whether a host may have a certificate issued,
# sending the hostname as a `host` query parameter and provisioning only on a 200
# response. The value is a path served by the app, or an absolute http(s) URL.
#
# Requires `ssl: true`, and cannot be combined with `host`/`hosts` or a custom SSL
# certificate. The app becomes the wildcard target, so it must approve its own
# domain too.
tls_on_demand_url: /up/tls_on_demand

# Custom SSL certificate
#
# In some cases, using Let's Encrypt for automatic certificate management is not an
Expand Down
5 changes: 5 additions & 0 deletions lib/kamal/configuration/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def hosts
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
end

def tls_on_demand_url
proxy_config["tls_on_demand_url"]
end

def custom_ssl_certificate?
ssl = proxy_config["ssl"]
return false unless ssl.is_a?(Hash)
Expand Down Expand Up @@ -73,6 +77,7 @@ def deploy_options
tls: ssl? ? true : nil,
"tls-certificate-path": container_tls_cert,
"tls-private-key-path": container_tls_key,
"tls-on-demand-url": tls_on_demand_url,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes good call, I'm running this with a custom built image from main. Will ask for a release on kamal-proxy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"deploy-timeout": seconds_duration(config.deploy_timeout),
"drain-timeout": seconds_duration(config.drain_timeout),
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
Expand Down
32 changes: 31 additions & 1 deletion lib/kamal/configuration/validator/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,30 @@ def validate!
unless config.nil?
super

if config["host"].blank? && config["hosts"].blank? && config["ssl"]
if config["host"].blank? && config["hosts"].blank? && config["ssl"] && config["tls_on_demand_url"].blank?
error "Must set a host to enable automatic SSL"
end

if (config.keys & [ "host", "hosts" ]).size > 1
error "Specify one of 'host' or 'hosts', not both"
end

if config.key?("tls_on_demand_url")
unless config["ssl"]
error "Must enable ssl to use tls_on_demand_url"
end

if config["host"].present? || config["hosts"].present?
error "Cannot set a host when using tls_on_demand_url"
end

if config["ssl"].is_a?(Hash)
error "Cannot use a custom SSL certificate with tls_on_demand_url"
end

ensure_valid_tls_on_demand_url config["tls_on_demand_url"]
end

if config["ssl"].is_a?(Hash)
if config["ssl"]["certificate_pem"].present? && config["ssl"]["private_key_pem"].blank?
error "Missing private_key_pem setting (required when certificate_pem is present)"
Expand All @@ -36,6 +52,20 @@ def validate!
end

private
# Mirrors kamal-proxy's own parsing: a path, or an absolute http(s) URL with a host.
def ensure_valid_tls_on_demand_url(url)
if url.is_a?(String) && url.present?
return if url.start_with?("/") && !url.start_with?("//")

uri = URI.parse(url)
return if uri.scheme.in?([ "http", "https" ]) && uri.host.present?
end

error "tls_on_demand_url must be a path or an absolute http(s) URL"
rescue URI::InvalidURIError
error "tls_on_demand_url must be a path or an absolute http(s) URL"
end

def ensure_valid_bind_ips(bind_ips)
bind_ips.present? && bind_ips.each do |ip|
next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex
Expand Down
54 changes: 54 additions & 0 deletions test/configuration/proxy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,60 @@ class ConfigurationProxyTest < ActiveSupport::TestCase
assert_not config.proxy.ssl?
end

test "tls_on_demand_url with no host" do
@deploy[:proxy] = { "ssl" => true, "tls_on_demand_url" => "/up/tls_on_demand" }
assert_equal "/up/tls_on_demand", config.proxy.tls_on_demand_url
assert_includes config.proxy.deploy_command_args(target: "172.1.0.2"), "--tls-on-demand-url=\"/up/tls_on_demand\""
end

test "tls_on_demand_url with an absolute url" do
@deploy[:proxy] = { "ssl" => true, "tls_on_demand_url" => "https://example.com/allowed" }
assert_equal "https://example.com/allowed", config.proxy.tls_on_demand_url
end

test "tls_on_demand_url omitted when not set" do
@deploy[:proxy] = { "ssl" => true, "host" => "example.com" }
assert_nil config.proxy.tls_on_demand_url
assert_not config.proxy.deploy_command_args(target: "172.1.0.2").any? { |arg| arg.start_with?("--tls-on-demand-url") }
end

test "tls_on_demand_url requires ssl" do
@deploy[:proxy] = { "tls_on_demand_url" => "/up/tls_on_demand" }
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
end

test "tls_on_demand_url cannot be combined with a host" do
@deploy[:proxy] = { "ssl" => true, "host" => "example.com", "tls_on_demand_url" => "/up/tls_on_demand" }
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
end

test "tls_on_demand_url cannot be combined with a custom certificate" do
@deploy[:proxy] = {
"ssl" => { "certificate_pem" => "CERTIFICATE_PEM", "private_key_pem" => "PRIVATE_KEY_PEM" },
"tls_on_demand_url" => "/up/tls_on_demand"
}
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
end

test "tls_on_demand_url accepts paths and absolute http(s) urls" do
[ "/up/tls_on_demand", "/", "https://example.com/allowed", "HTTP://example.com", "http://example.com:8080/a" ].each do |url|
@deploy[:proxy] = { "ssl" => true, "tls_on_demand_url" => url }
assert_equal url, config.proxy.tls_on_demand_url, "#{url} should be allowed"
end
end

test "tls_on_demand_url rejects anything else" do
[ "//example.com/allowed", "https://", "ftp://example.com", "example.com", "", 123, true ].each do |url|
@deploy[:proxy] = { "ssl" => true, "tls_on_demand_url" => url }
assert_raises(Kamal::ConfigurationError, "#{url.inspect} should be rejected") { config.proxy.ssl? }
end
end

test "a blank tls_on_demand_url is rejected rather than passed to the proxy" do
@deploy[:proxy] = { "ssl" => true, "host" => "example.com", "tls_on_demand_url" => "" }
assert_raises(Kamal::ConfigurationError) { config.proxy.deploy_command_args(target: "172.1.0.2") }
end

test "false not allowed" do
@deploy[:proxy] = false
assert_raises(Kamal::ConfigurationError, "proxy: should be a hash") do
Expand Down