-
Notifications
You must be signed in to change notification settings - Fork 32
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
AUT-54: Add endpoint to serve local x5u chains #917
Conversation
ab9c54f
to
ec81360
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably ought to be reviewed by @jmhodges or someone else that knows the codebase better, but the idea is certainly solid (and obviously desired by other stakeholders), and this implementation seems fine.
If I understand correctly, these files are written to ChainUploadLocation
over at
autograph/signer/contentsignaturepki/upload.go
Lines 30 to 31 in 5f97ba3
case "file": | |
return writeLocalFile(data, name, parsedURL) |
The most robust and production-like way to do this would probably be to add a mock s3 container (such as https://github.com/adobe/S3Mock), but that is both a) more work, and b) may not satisfy the needs of Remote Settings, so I suspect it's probably never going to be worthwhile to do.
handlers.go
Outdated
httpError(w, r, http.StatusMethodNotAllowed, "%s method not allowed; endpoint accepts GET only", r.Method) | ||
return | ||
} | ||
if r.Body != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of this code can be deleted. There's no Body
ever sent on GET
s and we shouldn't do anything with them, anyway. Go will handle the reading of bogus requests when we return the 405.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, technically this is incorrect. The HTTP protocol doesn't prohibit a GET
from providing a body, it is just that by convention no one does so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but I forgot to mention that the Go http library will close out the Body for us, anyhow, so it's still redundant.
handlers.go
Outdated
if r.Method != "GET" { | ||
httpError(w, r, http.StatusMethodNotAllowed, "%s method not allowed; endpoint accepts GET only", r.Method) | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already being handled in the routing later, no? That Methods("GET")
call?
(I know we have some other APIs doing this repetitive work, but I think that was a bit of a mistake. I could be convinced the other way, but I know this might all be mooted with the http.FileServer
work I describe below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed that there was a reason for having that code, so I simply copied what the other handlers were doing. However, if the routing layer will do this for us I agree it would be a lot cleaner and I can spend another cycle updating the rest of the handlers to drop the unsupported method cases and cleaning up the tests.
handlers_test.go
Outdated
name: "invalid method POST returns 405", | ||
method: "POST", | ||
url: "http://foo.bar/x5u/normandy/example.pem", | ||
nilBody: true, | ||
expectedStatus: http.StatusMethodNotAllowed, | ||
expectedBody: "POST method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", | ||
expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, | ||
}, | ||
{ | ||
name: "invalid method PUT returns 405", | ||
method: "PUT", | ||
url: "http://foo.bar/x5u/normandy/example.pem", | ||
nilBody: true, | ||
expectedStatus: http.StatusMethodNotAllowed, | ||
expectedBody: "PUT method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", | ||
expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, | ||
}, | ||
{ | ||
name: "invalid method OPTIONS returns 405", | ||
method: "OPTIONS", | ||
url: "http://foo.bar/x5u/normandy/example.pem", | ||
nilBody: true, | ||
expectedStatus: http.StatusMethodNotAllowed, | ||
expectedBody: "OPTIONS method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", | ||
expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, | ||
}, | ||
{ | ||
name: "invalid method HEAD returns 405", | ||
method: "HEAD", | ||
url: "http://foo.bar/x5u/normandy/example.pem", | ||
nilBody: true, | ||
expectedStatus: http.StatusMethodNotAllowed, | ||
expectedBody: "HEAD method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", | ||
expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally understand the desire here, but I think we want this HTTP method rejection work to be done in the routing layer. It feels a lil like "testing the framework". If you want, you could refactor the routing set up to make it easier to test?
handlers_test.go
Outdated
expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, | ||
}, | ||
{ | ||
name: "GET with directory parent escapes returns 400", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love these tests!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, all of these tests are no longer relevant :)
handlers_test.go
Outdated
expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, | ||
}, | ||
{ | ||
name: "GET with directory root escapes returns 400", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit (and so ignorable if you disagree):Since we make so many of these might be an opportunity to reduce the size of the test with a tiny helper func to generate these "GET for this particular API" structs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think a method would really clean this up any significant amount and we would still be typing out all the same things, just as method arguments instead of struct initialization values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I was just seeing that most of the fields on the structs were all the same except for like 3. But, again, a nit, and to your taste.
handlers.go
Outdated
chainFilePath := path.Join(parsedURL.Path, pathChainFile) | ||
stat, err := os.Stat(chainFilePath) | ||
if err != nil || !stat.Mode().IsRegular() { | ||
break | ||
} | ||
|
||
// Serve files from the chain upload location | ||
http.ServeFile(w, r, chainFilePath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote all the rest of this review and then got here and thought:
Stepping back, could some of this work be done with http.FileServer instead? Like, in the routing layer, we iterate over all of the getSigners
just once and create a http.FileServer(http.Dir(chainUploadLocationDir))
per keyid with a file://
chainUploadLocation? It would mean not having to do basically any of these checks since http.Dir and FileServer do them for us. The small cost is it would serve the subdirectories under those locations, but I think we're okay with that for dev and test.
This is a great idea! I think the " |
45f756c
to
8910ed7
Compare
I have refactored this work so that it now uses registers an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love how small this is getting! And thanks for investigating the Dir stuff! Have some questions about what's left and some tweaks (like the godoc and the extra var kicking around)
monitor_handler_racing_test.go
Outdated
body, _, err := contentsignaturepki.GetX5U(&http.Client{}, response.X5U) | ||
body, err := getMirroredX5U(response.X5U) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to do this change? I thought the new handler would be loaded here and available to be fetched from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding of the unit tests is that they are not starting the HTTP server, and we are running the tests by invoking the handlers directly. That used to be fine for the contentsignaturepki.GetX5U
because the file://
scheme would bypass any HTTP server anyways and just grab the certificate chain off the disk.
And this had to change again to work with the http.FileServer
implementation because there is no single handler function to call to fetch x5u chains anymore.
signer/signer.go
Outdated
// IDFormatRegexp is the compiled regex from IDFormat | ||
var IDFormatRegexp = regexp.MustCompile(IDFormat) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this can be removed now as it doesn't seem to be used
handlers.go
Outdated
@@ -74,6 +75,22 @@ func logSigningRequestFailure(sigreq formats.SignatureRequest, sigresp formats.S | |||
}).Info(fmt.Sprintf("signing operation failed with error: %v", err)) | |||
} | |||
|
|||
// If the x5u is a local file, mirror it via the `x5u` endpoint instead. | |||
func mirrorLocalX5U(r *http.Request, response formats.SignatureResponse) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm getting this confused with other similarly named things. Perhaps rewriteX5UIfLocalFile
? Something about rewriting, maybe. The mirror
sounds like it's copying the object from one place to another.
Also, nit: godoc format is to start the first sentence with the function's name
handlers.go
Outdated
@@ -74,6 +75,22 @@ func logSigningRequestFailure(sigreq formats.SignatureRequest, sigresp formats.S | |||
}).Info(fmt.Sprintf("signing operation failed with error: %v", err)) | |||
} | |||
|
|||
// If the x5u is a local file, mirror it via the `x5u` endpoint instead. | |||
func mirrorLocalX5U(r *http.Request, response formats.SignatureResponse) string { | |||
parsedX5U, err := url.Parse(response.X5U) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that there's less to look at I'm wondering: could we do this URL rewriting earlier in the process? That is to say, is it possible to rewrite the X5U URL on the signer at signer creation time in contentsignaturepki.New
or some such and not need all this happening at request time?
A couple follow-up questions I have after that: Would doing that rewrite earlier (if we can) allow us to simplify contentsignaturepki.GetX5U
? And would doing the url rewrite earlier also mean the getMirroredX5U
helper could go away since it doesn't need to know about files anymore? (I know I have a comment on getMirroredX5U
wondering about whether it needs to exist given the current state of the code, but I'm asking here in case that other question gets a "yes it needs to exist" answer)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory it can be done statically by changing the X5U in the signer config to match what gets served by http.FileServer
but to figure that out what the X5U should be, autograph would need to know its hostname to fully form the URL. And the hostname might not even be knowable at startup if it's running behind proxies or across container boundaries.
On the other hand, handling this at the time of the request is easy because we can just copy the hostname from the HTTP request headers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although, now that you mention it... all of this work should be acting upon config.X5U
which is the public URL intended for users to fetch content from where config.ChainUploadLocation
is the private URL to which chains are pushed. They just happen to have the same value for local testing.
Hypothetically, someone could setup a system where certs are saved locally to some dir, then a cron job uploads them somewhere else and users are expected to fetch the chains from the "somewhere else". This PR would break that hypothetical use case.
docs/endpoints.md
Outdated
|
||
This is an endpoint used to fetch certificate chains which are generated and | ||
stored locally. If the signer is configured with a `chainuploadlocation` with | ||
the `file://` scheme, then the contents of that file location are mirrored here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: a brief mention of only wanting to use this for local dev and testing might be nice
handlers.go
Outdated
@@ -203,6 +225,8 @@ func (a *autographer) handleSignature(w http.ResponseWriter, r *http.Request) { | |||
X5U: requestedSignerConfig.X5U, | |||
SignerOpts: requestedSignerConfig.SignerOpts, | |||
} | |||
sigresps[i].X5U = rewriteLocalX5U(r, sigresps[i]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this could be done in the struct set up but no more review cycles required to do that
Description
It has slightly bothered me that our integration tests, and local development experience frequently relies on
/tmp
being mounted into the container to serve certificate chains which are generated and stored locally using thefile://
scheme. It is very janky, inadequately documented, and can lead to a brittle development experience. This caught my attention when my/tmp
volume got corrupted with some stale state and it wound up breaking everything until I manually deleted the volume.It has also caused friction for downstream consumers of the Autograph container during development, such as remote-settings
I propose the following solution, we create a new endpoint:
/x5u/:keyid/:chainfile
which acts as a mirror to serve the contents of the configuredchainuploadlocation
for a signer, if, and only if, that signer has been configured with a local file upload location (eg: uses thefile://
scheme).Then, when generating signatures, we check if the signature contains an x5u URL with the file scheme, and replace it with the URL to the new
x5u
endpoint.This should mean that we no longer need to share
/tmp
between autograph containers, and we can eliminate the use of this volume altogether.I have added some tests for the endpoint, but I am not completely happy with the coverage as we would need a way to mock out the signers and mess around with their chain upload locations to really get full coverage.
Reference
See issue: #350 (AUT-54)