Skip to content

Comments

Secure cluster traffic via mutual TLS#2237

Merged
csmarchbanks merged 13 commits intoprometheus:mainfrom
hooten:tls
Aug 9, 2021
Merged

Secure cluster traffic via mutual TLS#2237
csmarchbanks merged 13 commits intoprometheus:mainfrom
hooten:tls

Conversation

@hooten
Copy link
Contributor

@hooten hooten commented Apr 20, 2020

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.Transport interface. 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.

@hooten hooten force-pushed the tls branch 4 times, most recently from b9f1ea4 to 36e6326 Compare April 20, 2020 17:48
@brian-brazil
Copy link
Contributor

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.

@hooten
Copy link
Contributor Author

hooten commented Apr 20, 2020

Happy to have the rest of it reviewed. We can clean up the TLS-Config code before merging.

Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

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

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 {
Copy link
Member

Choose a reason for hiding this comment

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

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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}
Copy link
Member

Choose a reason for hiding this comment

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

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?

Copy link
Contributor

@sharadgaur sharadgaur Apr 29, 2020

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

What sort of network latency did the testing have?

Copy link
Contributor

@sharadgaur sharadgaur May 1, 2020

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

The question isn't about how long things take over localhost, it's how many round trips may be required over a 200ms+ connection.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We've added a connection pool :)

@csmarchbanks
Copy link
Member

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?

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.

@sharadgaur sharadgaur force-pushed the tls branch 3 times, most recently from b249731 to edac61c Compare May 5, 2020 16:44
@hooten hooten force-pushed the tls branch 2 times, most recently from d85cd6e to b79abcc Compare May 5, 2020 17:15
@sharadgaur sharadgaur force-pushed the tls branch 3 times, most recently from 8458f49 to e79d7c4 Compare May 6, 2020 01:55
@stale stale bot added the stale label Jul 5, 2020
@stale stale bot removed the stale label Dec 30, 2020
@hooten hooten force-pushed the tls branch 8 times, most recently from d3fcaa4 to 86140f9 Compare January 1, 2021 18:52
@hooten
Copy link
Contributor Author

hooten commented Jan 12, 2021

I've made the requested updates. I'd appreciate another review @brian-brazil @mxinden @csmarchbanks. Thanks!

@hooten hooten requested a review from brian-brazil January 12, 2021 19:16
if err != nil {
return nil, err
}
pool.cache.Add(key, conn)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@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?

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 yes as two concurrent operations could each dial a TLS connection and then add the connection for the same key?

Copy link
Member

Choose a reason for hiding this comment

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

You might be able to use PeekOrAdd to avoid a mutex, but it would also have a bit of extra complexity. Basically:

  1. Do the get
  2. If not found, dial a TLS connection
  3. Use PeekOrAdd
  4. If found, use the "old" connection returned by PeekOrAdd, if added use the created connection.

Whichever way looks cleaner is fine by me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

hooten and others added 12 commits August 5, 2021 16:50
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>
Copy link
Member

@csmarchbanks csmarchbanks left a comment

Choose a reason for hiding this comment

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

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!

@csmarchbanks csmarchbanks merged commit ff85bec into prometheus:main Aug 9, 2021
@hooten hooten deleted the tls branch August 10, 2021 01:41
@hooten
Copy link
Contributor Author

hooten commented Aug 10, 2021

🎉 Thanks @csmarchbanks @gotjosh @beorn7 @mxinden @brian-brazil @sharadgaur !

@csmarchbanks
Copy link
Member

Thanks @hooten for all your work and commitment on this PR!

@markmsmith
Copy link

markmsmith commented Aug 12, 2021

+100, this is awesome, thank you!
Now that this has landed, is there a release planned soon, or are there any other blocking issues?

@pracucci pracucci mentioned this pull request Sep 15, 2021
3 tasks
nekketsuuu pushed a commit to nekketsuuu/alertmanager that referenced this pull request Oct 1, 2021
* 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>
@osipov-vadim
Copy link

osipov-vadim commented Mar 4, 2022

Hello! Thanks a lot for adding this feature, seems like a very great thing!

I have a few questions;

  1. What format would TLS Config file be? I tried to follow up with few formats, and it doesn't seem that those are the right ones.
  2. Considering question above , when I pass --cluster.tls-config flag down to AM, it fails to start. Error is: level=error ts=2022-03-04T21:21:25.340Z caller=coordinator.go:118 component=configuration msg="Loading configuration file failed" file=alertmanager.yml err="open alertmanager.yml: no such file or directory" Where file=alertmanager.yml is not the path that was specified by a line to start it. Would that be because config file possibly wrong?
  3. Is this feature enabled in by default in Alertmanager, or I need to download additional libraries? Not entirely clear.

I would highly appreciate advice

@Scrin
Copy link

Scrin commented Mar 4, 2022

  1. An example TLS config file can be found here
  2. The main config file (typically alertmanager.yml, provided via --config.file) is different form the TLS config file, make sure you are not mixing them up or missing the main config file. Something like:
    /bin/alertmanager --config.file=/etc/alertmanager/alertmanager.yml --cluster.tls-config=/etc/alertmanager/tls.yml (and any other cli flags) should work
  3. I believe it should be enabled by default in AM if you provide a valid configuration, at least on the docker image the config is all you need

@osipov-vadim
Copy link

osipov-vadim commented Mar 7, 2022

@Scrin Thanks a lot for reply!
Yeah, seems that I totally did not see the document there. Though was googling and searching for the tls config file example. Exactly what I needed!

I am not using a ready made docker image, I'm building my own. At this point I get an error alertmanager: error: unknown long flag '--cluster.tls-config', try --help where --help does not list --cluster.tls-config flag at all. Alertmanager version 0.23.0

Maybe I've missed a step in installation process?

Update:
Also tried passing the --cluster.tls-config to this image:
docker run --name alertmanager -d -p 127.0.0.1:9093:9093 quay.io/prometheus/alertmanager
seem to get exactly same thing, AM doesn't know about --cluster.tls-config flag.

@Scrin
Copy link

Scrin commented Mar 8, 2022

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 quay.io/prometheus/alertmanager:main or prom/alertmanager:main docker image for that)

}{
{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"},
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Member

Choose a reason for hiding this comment

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

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).

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.

Securing the gossip protocol?