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

[WIP] Fixes #23211 - Token based PuppetCA autosigning #576

Closed
wants to merge 1 commit into from

Conversation

juliantodt
Copy link
Member

@juliantodt juliantodt commented Apr 18, 2018

Removes old autosigning endpoints and adds new ones
that take a incoming CSR from puppet, extract the token
and forward it to foreman for verfication.

See also PRs in foreman-core, community-templates, puppet-foreman_proxy & puppet-puppet.

We would like to see some feedback early on while we are testing this thoroughly.

@dmitri-d
Copy link
Member

Please do not remove old api endpoints, log a warning about them being deprecated instead. We usually keep api for one or two releases after the deprecation announcement.

get "/autosign" do
content_type :json
post "/autosign" do
params = JSON.parse(request.env["rack.input"].read)
Copy link
Member

Choose a reason for hiding this comment

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

Since the api is being changed, why not change it to something like:

post "/autosign/:csr" instead of JSON encoding and then decoding a single parameter.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm pretty sure that CSRs are not URL-safe, as they include slashes and line-breaks.

Copy link
Member

Choose a reason for hiding this comment

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

You could UR-encode the parameter? Alternatively, instead of putting the parameter in the url, you could put it in the body of the request (would still need to be url-encoded though), which is a bit cleaner.

@csr.attributes.map do |attr|
{
oid: attr.oid,
value: attr.value.value.first.value
Copy link
Member

Choose a reason for hiding this comment

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

This looks fragile: are all of the intermediate calls guaranteed to not return nils?

Copy link
Member

Choose a reason for hiding this comment

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

I think this should be fine. Openssl is not known for its nice code.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, would probably be better to catch possible errors from this.

logger.debug "Request did not include a CSR"
return false
end
return true if signall
Copy link
Member

Choose a reason for hiding this comment

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

Is signall needed? If so, please put warnings both when it's triggered and during initialization.

Copy link
Member Author

Choose a reason for hiding this comment

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

signall is the equivalent of putting * in your autosign.conf at the moment. As there are currently people using this it was a requested feature, so we're adding this as a setting. I don't think we need warnings for this as it is disabled by default and only is used if you manually update the settings, so you need to know what you're doing.

Copy link
Member

Choose a reason for hiding this comment

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

I think a warning would be a good thing -- people forget to switch things back all the time. This flag completely circumvents token authorization, a warning would be a nice reminder (if verbose).

v =~ /^\s*#.*|^$/ ## Remove comments and empty lines
end.map do |v|
v.chomp ## Strip trailing spaces
def foreman_callback token
Copy link
Member

Choose a reason for hiding this comment

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

Could you use something more descriptive for the method name, something like csr_recognised?

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do.

def foreman_callback token
logger.debug "Calling foreman with token '#{token}'"
fr = Proxy::HttpRequest::ForemanRequest.new
req = fr.request_factory.create_delete("api/puppetca_token", :id => token)
Copy link
Member

Choose a reason for hiding this comment

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

I would suggested splitting the check and cleanup into two separate actions: not only removal of data on check not something that is expected, but it might make sense to keep the token until the host has been successfully created.

Copy link
Member

Choose a reason for hiding this comment

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

Removing the token is part of the design. For security reasons, it should just be able to use the token once.

Copy link
Member Author

Choose a reason for hiding this comment

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

In what scenario do you think would it make sense to still have the token after the CSR was verified with it? If anything goes wrong during the verification, the token will not be deleted, so we just deleted it when the host has his cert signed, when we don't need the token anymore.

Copy link
Member

Choose a reason for hiding this comment

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

It felt to me that this is a call with a (big) side-effect, which also broke the pattern of how we use "delete" method in smart-proxy.

If you renamed foreman_callback to something like delete_token, this would resolve the whole side-effect issue.

return true
end
rescue => e
logger.debug "Error while calling foreman: " + e.to_s
Copy link
Member

Choose a reason for hiding this comment

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

Please log this at the error level.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will change that. Do you think that the other logs are correct on debug? Considering you normally don't run it on debug-loglevel, you won't be able to trace back why a CSR wasn't verified if you wanted.

Copy link
Member

Choose a reason for hiding this comment

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

I'm ok either way, but perhaps a warning would be a bit more helpful.

@dmitri-d
Copy link
Member

I would also suggest replacing all class-methods in Proxy::PuppetCa with instance ones in a dedicated class, and use dependency injection to configure the module/api controller (please see dhcp, dns, or other modules for examples. Also see http://projects.theforeman.org/projects/foreman/wiki/How_to_Create_a_Smart-Proxy_Plugin#How-to-Load-Dependencies).

@@ -20,6 +20,12 @@ def create_get(path, query={}, headers={})
req
end

def create_delete(path, query={}, headers={})
req = Net::HTTP::Delete.new(uri(path).path + "/" + query[:id])
Copy link
Member

Choose a reason for hiding this comment

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

tap might come in handy here.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's the same as in all the other functions there...

@csr.attributes.map do |attr|
{
oid: attr.oid,
value: attr.value.value.first.value
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be fine. Openssl is not known for its nice code.

autosign.puts certname unless found
end
logger.debug "Added #{certname} to autosign"
def signall
Copy link
Member

Choose a reason for hiding this comment

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

Can we rename this to sign_all? I always read it as signal and start wondering where the second l comes from.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do.

def foreman_callback token
logger.debug "Calling foreman with token '#{token}'"
fr = Proxy::HttpRequest::ForemanRequest.new
req = fr.request_factory.create_delete("api/puppetca_token", :id => token)
Copy link
Member

Choose a reason for hiding this comment

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

Removing the token is part of the design. For security reasons, it should just be able to use the token once.

@juliantodt
Copy link
Member Author

@witlessbird
Removing old API-Endpoints: Puppet use the autosign.conf XOR the autosign-script. When we change to the script, the autosign.conf will be useless and with it all functions editing it. Sure we could keep the API endpoints and log warnings but they will have no effect aswell.

Outsourcing Methods: I will take a look at this.

Also some comments inline.
Could you take a look at the test? I don't think the fails are related to my changes, are they?

@timogoebel
Copy link
Member

Could you take a look at the test? I don't think the fails are related to my changes, are they?

Don't worry, [test] failures are unrelated.

req = fr.request_factory.create_delete("api/puppetca_token", :id => token)
begin
response = fr.send_request(req)
if response.code.to_i == 204
Copy link
Member

Choose a reason for hiding this comment

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

I'd say this is too specific, any 2xx would be ok?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since we write the Foreman-API at the same time, why not be specific?

Copy link
Member

Choose a reason for hiding this comment

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

Foreman side of things can change which would require this code to be updated. Since we don't care about particulars of the response as long as it is an "ok" (aka 2xx) one, making it too specific makes it more fragile.

Copy link
Member

@timogoebel timogoebel left a comment

Choose a reason for hiding this comment

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

@juliantodt: Thanks. Some more comments that came up when I read the code. The DI stuff looks good now.

# parameter is csr to use
def autosign csr
if csr.nil?
logger.warn "Request did not include a CSR"
Copy link
Member

Choose a reason for hiding this comment

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

Nit: missing .

@@ -0,0 +1,25 @@
module Proxy::PuppetCa
class CertificateRequest
Copy link
Member

Choose a reason for hiding this comment

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

The class name should match the name of the file.

@dmitri-d
Copy link
Member

dmitri-d commented May 2, 2018

Removing old API-Endpoints: Puppet use the autosign.conf XOR the autosign-script. When we change to the script, the autosign.conf will be useless and with it all functions editing it. Sure we could keep the API endpoints and log warnings but they will have no effect aswell.

If old behavior cannot be preserved at all, then all of these changes must reside in a new module/provider.

@timogoebel
Copy link
Member

If old behavior cannot be preserved at all, then all of these changes must reside in a new module/provider.

@witlessbird: Why is that? Do we want to continue to support the old behavior so that users can use a newer smart-proxy version with an older Foreman version? Or is this meant for all the standalone users who use smart-proxy without Foreman?

Removes old autosigning endpoints and adds new ones
that take a incoming CSR from puppet, extract the token
and forward it to foreman for verfication.
@juliantodt
Copy link
Member Author

Took care of all the comments as well as the test failures.

@witlessbird I can see that removing the old functionality might seem problematic, but it will be same with the updates to the foreman-core and I don't think we need to support older foreman versions with newer smart-proxy versions. Also allowing both signing options to be chosen by the user (-> will need to allow this in the foreman-core as well), will just cause problems like foreman & smart-proxy using tokens but puppet is configured to use the autosign.conf. When we are strict with these new changes and remove old functions we get obvious error messages at misconfigurations and the solution will be to just update to the latest version. I know it's not perfect to make these changes that affect multiple components that are backwards-incompatible, but I prefer it over the alternatives.

@ekohl
Copy link
Member

ekohl commented May 3, 2018

I agree with @witlessbird that the API shouldn't change behavior of endpoints. If it changes, then it should use other URLs so Foreman gets a clear error. There are good reasons for this: until now we've always been flexible with versions. Running a 1.17 proxy with a 1.16 foreman worked, but also a 1.16 proxy with a 1.17 foreman. We know there are users with many proxies and upgrading them all at once is not feasible.

That said, I think there's still room for a compatible API. The one that's currently changed is GET /autosign. It could return an empty list.

@dmitri-d
Copy link
Member

dmitri-d commented May 3, 2018

What @ekohl said. There is also a number of people who use smart-proxy api for integration with their own software -- they'll be affected by such a break too.

@juliantodt
Copy link
Member Author

juliantodt commented May 7, 2018

@witlessbird @ekohl So what you're saying is that you don't necessarily have a problem with removing old endpoints in favour of new ones but with keeping some and changing how they work. So change the name of the /autosign endpoint and done?

@dmitri-d
Copy link
Member

dmitri-d commented May 7, 2018

@juliantodt: not quite. Old endpoints have to keep names and preserve functionality for a couple releases after being declared as deprecated.

If new functionality is being added to an existing end-point, the old behaviour must also be preserved.

@timogoebel
Copy link
Member

@juliantodt: It looks like there is a lot of resistance in getting this in, which is unfortunate as we did ask if there were any concerns before starting the implementation. I'd suggest to split this up and try to get the refactorings in (moving the code to a provider) so the puppetca api is pluggable. Maybe we can then either support both providers or move this to a plugin.

@juliantodt
Copy link
Member Author

Closing this in favour of the pluggable, backwards compatible approach #586.

@juliantodt juliantodt closed this Jun 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants