Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement the On-Demand TLS feature #63

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

Conversation

did
Copy link

@did did commented Nov 1, 2024

Big fan of Kamal & Kamal proxy here. Great job 👏

However, for my 2 Rails projects (website hosting for LocomotiveCMS & Maglev), I was missing the On-Demand TLS feature in Caddy.

It turns out it was easy to implement it in Kamal-Proxy since the autocert.HostPolicy type is just a function returning an error if the host is not allowed to get a certificate.

So, just by setting a new config option (--tls-on-demand-url), we can test dynamically if a host can get a TLS certificate, just by calling the --tls-on-demand-url endpoint.
It just has to return a 200 HTTP code (http://my-api-end-point/any-path?host=).

In order to test it on my server, I implemented a little Sinatra service to test but I didn't include in my PR because it was in Ruby and the single example in your repository was written in Go. Besides, this is a super niche feature, so it might not require an example.

require 'sinatra'

ALLOWED_HOSTS = %w(paul.nocoffee.fr sacha.nocoffee.fr)

get '/' do
  <<-HTML 
<html>
  <body>
    <h1>Hello #{request.host}!</h1>
  </body>
</html>
  HTML
end

get '/up' do
  200
end

get '/check' do
  ALLOWED_HOSTS.include?(params[:host]) ? [200, ['ok']] : [406, ['fail']]
end

My next step is to build a Kamal proxy docker image and use it in the Kamal deployment of one of my projects.

Let me know if you want me to re-work the PR to match your PR rules.

Thanks!

@did
Copy link
Author

did commented Nov 3, 2024

alright, I've made it work with my own fork of kamal.

The modifications were super light, actually it was just a matter of accepting a new config option for kamal-proxy (+ point to my own Docker image of kamal-proxy).

So, my config/deploy.yml looks like:

proxy:
  ssl: true
  hosts: [""]
  tls_on_demand_url: "http://staging-app-web-latest:3000/locomotive/api/allowed_host"
  app_port: 3000
  forward_headers: true

Notes:

  • hosts: [""] is important. Basically, it tricks the kamal-proxy router by allowing the * (wildcard) route (originally, impossible with ssl: true). I'd rather put hosts: "*" instead but it would work some extra work on the kamal-proxy app.
  • staging-app-web-latest is the host of my app, following the Kamal app name pattern <service_name>-web-<version>. I had to set a specific of version to make it work across deployments (export VERSION=latest).

@did
Copy link
Author

did commented Nov 29, 2024

I also needed this feature to run the LocomotiveCMS hosting platform behind Kamal-Proxy 👉 did#1

@did
Copy link
Author

did commented Dec 2, 2024

thanks @kwilczynski for the code review!

Copy link
Collaborator

@kevinmcconnell kevinmcconnell left a comment

Choose a reason for hiding this comment

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

Hey @did,

Thanks for getting this started! This does seem like a useful addition.

I left a couple of comments on the details. Let me know what you think about addressing those. (I'm also happy to make the changes myself if you don't have time).

from any host allowed by an external API endpoint of your choice.
This avoids hard-coding hosts in the configuration, especially when you don't know the hosts at the startup.

kamal-proxy deploy service1 --target web-1:3000 --host "" --tls --tls-on-demand-url="http://localhost:4567/check"
Copy link
Collaborator

Choose a reason for hiding this comment

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

We shouldn't need to set an empty --host here. We could modify this restriction so that --host is not required with the On Demand feature.

This probably applies when using custom certificates as well, come to think of it.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed! But internally, we should use the empty string (or any *) as the host for the internal router, right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, exactly! If it’s empty it will match everything, so if they are using on-demand TLS we can let them omit the flag to do that.

Wildcard routing could still be useful to allow multiple apps to run side by side, even if one or more of them is using on-demand TLS. So people should be allowed to specify the host string when they want. It’s just optional in that case.

return autocert.HostWhitelist(hosts...), nil
}

_, err := url.ParseRequestURI(options.TLSOnDemandUrl)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd expect that the on demand URL will usually point to an endpoint in the application that's deployed, rather than to some other external app. In which case, it would be simpler for this to be a path rather than an absolute URL. We'd then automatically call it on the currently deployed target (a bit like how the health check paths work).

That way you don't have to worry about having a stable hostname to reach for all versions of the app, etc., because the proxy takes care of that for you.

I'm not sure if there's a common enough need to support an external on demand URL as well, but for simplicity's sake it would be nice to have this be path-only if possible.

What do you think?

Copy link
Author

Choose a reason for hiding this comment

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

👌 I haven't thought about it!!!

In my production case, it would work perfectly because my Rails app was always in charge of returning that list of hostnames (even when I was hosting it with k8s).
Indeed, it'd have saved me a lot of time, trying to figure out the hostname of my endpoint.

Perhaps (it's a guess), we should keep the URL as well for developers who prefers to move the responsibility of this endpoint to another app (and probably deployed by Kamal too) for performance or architecture reasons.

Let's keep it simple in a first time so let's use the path only :-)

(I will make the modifications)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sounds good!

return func(ctx context.Context, host string) error {
slog.Info("Get a certificate", "host", host)

resp, err := http.Get(fmt.Sprintf("%s?host=%s", options.TLSOnDemandUrl, url.QueryEscape(host)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be worth having a timeout on this request. I don't think it needs to be user-configurable; just something reasonable like a couple seconds.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed. I'm wondering that even 2 or 2 seconds is probably too long and the TLS certificate generation will fail.

(I'll make the modifications)

Copy link
Collaborator

@kevinmcconnell kevinmcconnell Dec 17, 2024

Choose a reason for hiding this comment

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

I think a couple of seconds should work OK, it just all leads to a slower experience for the client for the first request when a new cert is first generated. The initial cert generation typically takes a few seconds anyway. But, it could be worth testing that out to be sure.

Client: &acme.Client{DirectoryURL: options.ACMEDirectory},
}, nil
}

func (s *Service) createAutoCertHostPolicy(hosts []string, options ServiceOptions) (autocert.HostPolicy, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would be nice to have some test coverage of this, now that's getting a little more involved than just a HostWhiteList. Same with the options parsing.

Copy link
Author

Choose a reason for hiding this comment

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

Sure. I'll try to write some.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants