Skip to content

Commit 8a7260d

Browse files
authored
Merge pull request #1531 from acidtib/feat/custom-ssl
feat: Add support for custom certificates
2 parents 89c5691 + 99f763d commit 8a7260d

File tree

16 files changed

+196
-14
lines changed

16 files changed

+196
-14
lines changed

lib/kamal/cli/app.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def boot
1212

1313
KAMAL.roles_on(host).each do |role|
1414
Kamal::Cli::App::Assets.new(host, role, self).run
15+
Kamal::Cli::App::SslCertificates.new(host, role, self).run
1516
end
1617
end
1718

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class Kamal::Cli::App::SslCertificates
2+
attr_reader :host, :role, :sshkit
3+
delegate :execute, :info, to: :sshkit
4+
5+
def initialize(host, role, sshkit)
6+
@host = host
7+
@role = role
8+
@sshkit = sshkit
9+
end
10+
11+
def run
12+
if role.running_proxy? && role.proxy.custom_ssl_certificate?
13+
info "Writing SSL certificates for #{role.name} on #{host}"
14+
execute *app.create_ssl_directory
15+
if cert_content = role.proxy.certificate_pem_content
16+
execute *app.write_certificate_file(cert_content)
17+
end
18+
if key_content = role.proxy.private_key_pem_content
19+
execute *app.write_private_key_file(key_content)
20+
end
21+
end
22+
end
23+
24+
private
25+
def app
26+
@app ||= KAMAL.app(role: role, host: host)
27+
end
28+
end

lib/kamal/commands/app/proxy.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ def remove_proxy_app_directory
2121
remove_directory config.proxy_boot.app_directory
2222
end
2323

24+
def create_ssl_directory
25+
make_directory(config.proxy_boot.tls_directory)
26+
end
27+
28+
def write_certificate_file(content)
29+
[ :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") ]
30+
end
31+
32+
def write_private_key_file(content)
33+
[ :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") ]
34+
end
35+
2436
private
2537
def proxy_exec(*command)
2638
docker :exec, proxy_container_name, "kamal-proxy", *command

lib/kamal/configuration.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def initialize(raw_config, destination: nil, version: nil, validate: true)
6363
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
6464

6565
@logging = Logging.new(logging_config: @raw_config.logging)
66-
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy)
66+
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
6767
@proxy_boot = Proxy::Boot.new(config: self)
6868
@ssh = Ssh.new(config: self)
6969
@sshkit = Sshkit.new(config: self)

lib/kamal/configuration/accessory.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ def initialize_proxy
125125
Kamal::Configuration::Proxy.new \
126126
config: config,
127127
proxy_config: accessory_config["proxy"],
128-
context: "accessories/#{name}/proxy"
128+
context: "accessories/#{name}/proxy",
129+
secrets: config.secrets
129130
end
130131

131132
def initialize_registry

lib/kamal/configuration/docs/proxy.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# run on the same proxy.
1212
#
1313
proxy:
14-
1514
# Hosts
1615
#
1716
# The hosts that will be used to serve the app. The proxy will only route requests
@@ -45,7 +44,21 @@ proxy:
4544
# unless you explicitly set `forward_headers: true`
4645
#
4746
# Defaults to `false`:
48-
ssl: true
47+
ssl: ...
48+
49+
# Custom SSL certificate
50+
#
51+
# In some cases, using Let's Encrypt for automatic certificate management is not an
52+
# option, or you may already have SSL certificates issued by a different
53+
# Certificate Authority (CA). Kamal supports loading custom SSL certificates
54+
# directly from secrets.
55+
#
56+
# Examples:
57+
# ssl: true # Enable SSL with Let's Encrypt
58+
# ssl: false # Disable SSL
59+
# ssl: # Enable custom SSL
60+
# certificate_pem: CERTIFICATE_PEM
61+
# private_key_pem: PRIVATE_KEY_PEM
4962

5063
# SSL redirect
5164
#

lib/kamal/configuration/proxy.rb

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ class Kamal::Configuration::Proxy
66

77
delegate :argumentize, :optionize, to: Kamal::Utils
88

9-
attr_reader :config, :proxy_config
9+
attr_reader :config, :proxy_config, :secrets
1010

11-
def initialize(config:, proxy_config:, context: "proxy")
11+
def initialize(config:, proxy_config:, secrets:, context: "proxy")
1212
@config = config
1313
@proxy_config = proxy_config
1414
@proxy_config = {} if @proxy_config.nil?
15+
@secrets = secrets
1516
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
1617
end
1718

@@ -27,10 +28,42 @@ def hosts
2728
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
2829
end
2930

31+
def custom_ssl_certificate?
32+
ssl = proxy_config["ssl"]
33+
return false unless ssl.is_a?(Hash)
34+
ssl["certificate_pem"].present? && ssl["private_key_pem"].present?
35+
end
36+
37+
def certificate_pem_content
38+
ssl = proxy_config["ssl"]
39+
return nil unless ssl.is_a?(Hash)
40+
secrets[ssl["certificate_pem"]]
41+
end
42+
43+
def private_key_pem_content
44+
ssl = proxy_config["ssl"]
45+
return nil unless ssl.is_a?(Hash)
46+
secrets[ssl["private_key_pem"]]
47+
end
48+
49+
def certificate_pem
50+
tls_file_path("cert.pem")
51+
end
52+
53+
def private_key_pem
54+
tls_file_path("key.pem")
55+
end
56+
57+
def tls_file_path(filename)
58+
File.join(config.proxy_boot.tls_container_directory, filename) if custom_ssl_certificate?
59+
end
60+
3061
def deploy_options
3162
{
3263
host: hosts,
33-
tls: proxy_config["ssl"].presence,
64+
tls: ssl? ? true : nil,
65+
"tls-certificate-path": certificate_pem,
66+
"tls-private-key-path": private_key_pem,
3467
"deploy-timeout": seconds_duration(config.deploy_timeout),
3568
"drain-timeout": seconds_duration(config.drain_timeout),
3669
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
@@ -68,7 +101,7 @@ def stop_command_args(**options)
68101
end
69102

70103
def merge(other)
71-
self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config)
104+
self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config), secrets: secrets
72105
end
73106

74107
private

lib/kamal/configuration/proxy/boot.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ def error_pages_container_directory
100100
File.join app_container_directory, "error_pages"
101101
end
102102

103+
def tls_directory
104+
File.join app_directory, "tls"
105+
end
106+
107+
def tls_container_directory
108+
File.join app_container_directory, "tls"
109+
end
110+
103111
private
104112
def ensure_valid_bind_ips(bind_ips)
105113
bind_ips.present? && bind_ips.each do |ip|

lib/kamal/configuration/role.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ def asset_volume_directory(version = config.version)
150150
end
151151

152152
def ensure_one_host_for_ssl
153-
if running_proxy? && proxy.ssl? && hosts.size > 1
154-
raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}"
153+
if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?
154+
raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}"
155155
end
156156
end
157157

@@ -173,6 +173,7 @@ def initialize_specialized_proxy
173173
@specialized_proxy = Kamal::Configuration::Proxy.new \
174174
config: config,
175175
proxy_config: proxy_config,
176+
secrets: config.secrets,
176177
context: "servers/#{name}/proxy"
177178
end
178179
end

lib/kamal/configuration/validator.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ def validate_against_example!(validation_config, example)
2424
example_value = example[key]
2525

2626
if example_value == "..."
27-
unless key.to_s == "proxy" && boolean?(value.class)
27+
if key.to_s == "ssl"
28+
validate_type! value, TrueClass, FalseClass, Hash
29+
elsif key.to_s != "proxy" || !boolean?(value.class)
2830
validate_type! value, *(Array if key == :servers), Hash
2931
end
3032
elsif key == "hosts"

0 commit comments

Comments
 (0)