Secure cluster traffic via mutual TLS#2237
Conversation
|
This PR should not be considered for merging until the tls code is in common, it is not planned to ever have more than 1 copy of that code. |
|
Happy to have the rest of it reviewed. We can clean up the TLS-Config code before merging. |
mxinden
left a comment
There was a problem hiding this comment.
Great that this is happening!
I added two comments inline.
In my proof-of-concept I extracted the memberlist specific logic to a separate project for other memberlist users to reuse. What do you think about doing the same here? Is the additional complexity worth the effort?
|
|
||
| // writePacket writes all the bytes in one operation so no concurrent write happens in between. | ||
| // It prefixes the connection type, the from address and the message length. | ||
| func writePacket(conn net.Conn, fromAddr string, b []byte) error { |
There was a problem hiding this comment.
To future-proof this protocol what do you think of sending a version number down the wire?
Protobuf would do length delimiting and versioning. Do you think that would be overkill?
There was a problem hiding this comment.
We made a change to use protobuf for this. Any thoughts? Are there ways we could improve it? Thanks!
| // writes to it, and closes it. It also returns a time stamp of when | ||
| // the packet was written. | ||
| func (t *TLSTransport) WriteTo(b []byte, addr string) (time.Time, error) { | ||
| dialer := &net.Dialer{Timeout: DefaultTcpTimeout} |
There was a problem hiding this comment.
Do I understand correctly, that this is opening up a new TCP connection for each packet to send? What do you think of something along the lines of a connection pool?
There was a problem hiding this comment.
It adds additional complexity as we need to manage the lifecycle of the connection and the Memberlist does not provide any good way to handle it. I would not recommend using a connection pool in this case.
In our testing, we did not see any issues yet. If you like we can run additional load tests.
There was a problem hiding this comment.
What sort of network latency did the testing have?
There was a problem hiding this comment.
An individual request is taking less than 3 mill seconds. Here are the benchmark results
goos: darwin
goarch: amd64
pkg: github.com/prometheus/alertmanager/cluster
BenchmarkWriteTo-12 1000000000 0.00243 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/prometheus/alertmanager/cluster 0.111s
Success: Benchmarks passed.
There was a problem hiding this comment.
The question isn't about how long things take over localhost, it's how many round trips may be required over a 200ms+ connection.
There was a problem hiding this comment.
Oh got it. I will run a test against remotely deployed Alertmanger next week. If we see any issue we will implement a connection pool.
Thank you.
There was a problem hiding this comment.
We've added a connection pool :)
We already moved tsdb back inside of Prometheus to avoid dealing with version upgrades and such across projects. It is up to the maintainers of Alertmanager, but I would keep the lessons of tsdb in mind. |
|
I've made the requested updates. I'd appreciate another review @brian-brazil @mxinden @csmarchbanks. Thanks! |
|
I've only taken a quick peek, and noticed that there's no config for the client-side parts of this. You should use https://github.com/prometheus/common/blob/20c99e7aa07352b599bd0517cd5ca945f4af0407/config/http_config.go#L325 for that. |
|
Thanks! I will update it to use that one. |
| key_file: "certs/node1-key.pem" | ||
| client_ca_file: "certs/ca.pem" | ||
| client_auth_type: "VerifyClientCertIfGiven" | ||
| tls_client_config: |
There was a problem hiding this comment.
@brian-brazil. Added this commit. Is this the sort of config that you suggested I add? Or was it something else? Thanks!
There was a problem hiding this comment.
That looks right from a quick peek.
| "io/ioutil" | ||
| "path/filepath" | ||
|
|
||
| common "github.com/prometheus/common/config" |
There was a problem hiding this comment.
Don't rename imports unless it's unavoidable, it makes it harder to grep and understand the code.
| return nil, err | ||
| } | ||
| config := &TLSTransportConfig{ | ||
| TLSServerConfig: &web.TLSStruct{ |
There was a problem hiding this comment.
You shouldn't be tweaking any of this, leave it up to the library. A tls config should have the same defaults everywhere its used.
There was a problem hiding this comment.
removed this configuration. Now just unmarshaling
| key_file: "certs/node1-key.pem" | ||
| client_ca_file: "certs/ca.pem" | ||
| client_auth_type: "VerifyClientCertIfGiven" | ||
| tls_client_config: |
There was a problem hiding this comment.
That looks right from a quick peek.
|
@simonpasquier I'd appreciate a review whenever you get a chance. |
|
Hey y'all, we would love to see this merged! Any updates on when that will happen? |
|
@gotjosh would this be something for you to look at, to at the same time get more familiar with the codebase? |
|
I'm happy to take a look at it eventually but my current backlog seems to not finish. This is quite a big change though, and I have 3 other things here (1 PR open, 1 to review and another to implement 😄 ). I'll add it to my list but I don't expect this to be soonish. |
|
BTW, the test failures seem real (expired certificate) rather than just flaky. |
|
Yes, I noticed that. Busy week at work. I will fix it soon |
|
The expired certs issue has been fixed. |
|
I would appreciate it if someone could review this. |
|
@gotjosh @simonpasquier what is your state on this? If you cannot review this any time soon, can you delegate this to someone else at Red Hat or Grafana who is invested here? |
csmarchbanks
left a comment
There was a problem hiding this comment.
I left a few minor comments to discuss/address, but nothing blocking. Since this is an additive change and all TLS support is already marked as experimental I move to merge this soon, and allow more people to test out the functionality. We can always provide fixes in additional PRs as they come up.
| if err != nil { | ||
| return nil, err | ||
| } | ||
| pool.connections[key] = conn |
There was a problem hiding this comment.
It seems like this pool will grow forever as addresses come and go? Are there concerns that could cause issues, my gut is that it is unlikely as the number of addresses is probably small over the lifetime of an alertmanager?
There was a problem hiding this comment.
I suspect that the number of connections in the pool will remain small unless peer addresses change regularly/frequently. Maybe it would be worth declaring bankruptcy on all the connections in the pool once it reaches a certain size (e.g., 1000). Is there a better way?
There was a problem hiding this comment.
Probably the safest option would be a LRU with a max size that is sufficiently large, 1024 would make sense to me. Alternatively, you could periodically delete connections that have not been used for a long time.
You are right that the map probably would not grow to be very large, but it also feels not ideal to merge a known memory leak. Perhaps try the hashicorp lru library to see if that change is minor?
gotjosh
left a comment
There was a problem hiding this comment.
I apologise as I've been sitting on half of my review for a while, but overall the changes look good - my comments are mostly nits.
My take here is given we've cut a new version a couple of weeks ago and have had several patches to it accordingly, we can let this bake in master for a while before we cut a new version to ensure we have time to catch anything (if it comes!).
|
'm very sorry that it took so long to rally some reviewers, but @gotjosh and @csmarchbanks finally got to it a month ago. Both seem to just have a few minor nits. @hooten do you think you'll be able to address the comments? Then we could finally merge this and make it available for a somewhat wider public to try out. |
|
Yep, Thanks for the reviews! I will get to it this week |
|
@gotjosh, @csmarchbanks. Thanks again for the reviews. I pushed some fixes and left a couple comments/questions in response a few days ago. Let me know what you think. Thanks! |
|
Thanks @csmarchbanks, I will give that a try sometime in the next couple of days |
| type connectionPool struct { | ||
| connections map[string]*tlsConn | ||
| tlsConfig *tls.Config | ||
| lock sync.Mutex |
There was a problem hiding this comment.
| lock sync.Mutex | |
| mtx sync.Mutex |
last nit given you're about to touch this file as part of Chris's comments.
There was a problem hiding this comment.
I took away the mutex here because the lru cache provides its own locking mechanism, which I think suffices.
| // read returns a packet for packet connections or an error if there is one. | ||
| // It returns nothing if the connection is meant to be streamed. | ||
| func (conn *tlsConn) read() (*memberlist.Packet, error) { | ||
| if conn.connection == nil { |
There was a problem hiding this comment.
my understanding was that conn.mtx is protecting connection, any particular reason why we're not using the lock to read the connection here?
There was a problem hiding this comment.
Good catch. I looked at it more and I think the lock was incidentally unnecessary because read is only called after rcvTLSConn, which creates a new connection object.
However, I'm adding a lock around the reading of the connection to make this safer to use.
| err = conn.writeStream() | ||
| if err != nil { | ||
| t.writeErrs.WithLabelValues("stream").Inc() | ||
| return conn.connection, errors.Wrap(err, "failed to create stream connection") |
There was a problem hiding this comment.
Similar thoughts here - perhaps, we're assuming we want to sync writing but not reading - is this correct?
There was a problem hiding this comment.
I think this is a similar situation to the above. The connection is never reused so it is incidentally working. I'm adding a new function and some safety measures.
| if err != nil { | ||
| return nil, err | ||
| } | ||
| pool.cache.Add(key, conn) |
There was a problem hiding this comment.
@csmarchbanks I updated this to use an lru cache. I removed the mutex from this file because the cache provides locking. However, I'm wondering if I still need it between the pool.cache.Get and the pool.cache.Add. Thoughts?
There was a problem hiding this comment.
I think yes as two concurrent operations could each dial a TLS connection and then add the connection for the same key?
There was a problem hiding this comment.
You might be able to use PeekOrAdd to avoid a mutex, but it would also have a bit of extra complexity. Basically:
- Do the get
- If not found, dial a TLS connection
- Use PeekOrAdd
- If found, use the "old" connection returned by PeekOrAdd, if added use the created connection.
Whichever way looks cleaner is fine by me.
There was a problem hiding this comment.
I tried both ways. In the end, the mutex ended looking cleaner.
The PeekOrAdd would have been nice if I didn't always need to check whether the connection is alive when I get it back from the cache.
Co-authored-by: Sharad Gaur <sharadgaur@gmail.com> Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
Signed-off-by: Dustin Hooten <dustinhooten@gmail.com>
csmarchbanks
left a comment
There was a problem hiding this comment.
Re-ran the tests and they pass now, looks like a flaky acceptance test. 👍 from me, I will wait for Monday to see if any comments/concerns come through and will then plan to merge this!
|
🎉 Thanks @csmarchbanks @gotjosh @beorn7 @mxinden @brian-brazil @sharadgaur ! |
|
Thanks @hooten for all your work and commitment on this PR! |
|
+100, this is awesome, thank you! |
* Add TLS option to gossip cluster Co-authored-by: Sharad Gaur <sharadgaur@gmail.com> Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * generate new certs that expire in 100 years Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * Fix tls_connection attributes Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * Improve error message Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * Fix tls client config docs Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * Add capacity arg to message buffer Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * fix formatting Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * Update version; add version validation Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * use lru cache for connection pool Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * lock reading from the connection Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * when extracting net.Conn from tlsConn, lock and throw away wrapper Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * Add mutex to connection pool to protect cache Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> * fix linting Signed-off-by: Dustin Hooten <dustinhooten@gmail.com> Co-authored-by: Sharad Gaur <sharadgaur@gmail.com>
|
Hello! Thanks a lot for adding this feature, seems like a very great thing! I have a few questions;
I would highly appreciate advice |
|
|
@Scrin Thanks a lot for reply! I am not using a ready made docker image, I'm building my own. At this point I get an error Maybe I've missed a step in installation process? Update: |
|
0.23.0 was released before this was merged, so it's not available in that version yet, and the next version doesn't seem to be released yet. If you build AM from the main branch (instead of the v0.23.0 tag) you should get this feature (Docker users can use |
| }{ | ||
| {bindAddr: localhost, bindPort: 9094, inputIp: "10.0.0.5", inputPort: 54231, expectedIp: "10.0.0.5", expectedPort: 54231}, | ||
| {bindAddr: localhost, bindPort: 9093, inputIp: "invalid", inputPort: 54231, expectedError: "failed to parse advertise address \"invalid\""}, | ||
| {bindAddr: "0.0.0.0", bindPort: 0, inputIp: "", inputPort: 0, expectedIp: "random"}, |
There was a problem hiding this comment.
Such a test is not very packager-friendly. Most distros will run unit tests in clean room environment when packaging, which can often mean a host with only a loopback interface (with 127.0.0.1 and usually [::1] configured), and no default route.
On Debian buildbots, and presumably other distros with similar clean room package building policy, this fails with:
=== RUN TestFinalAdvertiseAddr
tls_transport_test.go:89:
Error Trace: /<<PKGBUILDDIR>>/build/src/github.com/prometheus/alertmanager/cluster/tls_transport_test.go:89
Error: Expected nil, but got: &errors.errorString{s:"no private IP address found, and explicit IP not provided"}
Test: TestFinalAdvertiseAddr
--- FAIL: TestFinalAdvertiseAddr (0.00s)
This is due to the bindAddr being "0.0.0.0" and the inputIp being empty.
I see that TestClusterJoinAndReconnect in cluster/cluster_test.go at least checks first whether a private IP exists, and skips the test if none is found. Perhaps it makes sense to omit this particular test case in such a scenario also.
There was a problem hiding this comment.
I see that TestClusterJoinAndReconnect in cluster/cluster_test.go at least checks first whether a private IP exists, and skips the test if none is found. Perhaps it makes sense to omit this particular test case in such a scenario also.
I agree and the skip in cluster_test.go exists exactly for the same constraint (#1445).
Co-authored-by: Sharad Gaur sharadgaur@gmail.com
Signed-off-by: Dustin Hooten dustinhooten@gmail.com
This pull request makes it possible to use mutual TLS for cluster communications.
By adding the path to a TLS configuration file using the command line flag
--cluster.tls-config, TLS will be used for inter-peer gossip.Mutual TLS is achieved by configuring Memberlist to use a TLS implementation of the
memberlist.Transportinterface. This approach has been submitted in a design doc and implemented as a proof-of-concept in #1819. This pull request continues that work.Closes #1322
Updates December 30, 2020: TLS server config uses common code from
prometheus/exporter-toolkit.Updates January 2021: TLS config also allows client config using code from
prometheus/common.