Implement per-log bastions - #35
Conversation
| {"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"]} | ||
| {"origin":"sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702+9bc54c7f+AVv6q3xDaHxI2aTemqEb7W6ZcbO7QbTqTr20thOqfqsw"]} | ||
| {"origin":"sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT"]} | ||
| {"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"],"bastions":[null]} |
There was a problem hiding this comment.
It's weird that I get "bastions":[null] rather than "bastions":[] or maybe "bastions":null. Maybe a log with no keys also has this behavior? I've found one hint of a potential workaround at https://sqlite.org/forum/info/5804b03c6ad78238c5a86323332c1a7ad38ccbc431b08b502fbc530d52ec5fdd, but I haven't tried it out.
There was a problem hiding this comment.
This would annoy me if I tried to process the resulting json, but I'd suggest leaving this as is for now (unless someone can suggest an easy fix).
There was a problem hiding this comment.
Fixed with an explicit COALESCE.
| } | ||
| e := make(chan error, 1) | ||
| // Handle log-specific bastions. | ||
| logBastions, err := w.AllBastions() |
There was a problem hiding this comment.
This only looks up configured bastions at startup, changes made with witnessctl won't take effect until next restart. I wonder if that is good enough, or if we need something more dynamic?
There was a problem hiding this comment.
There seems to be an sqlite3_update_hook function, but unclear to me if that is usable for this purpose.
There was a problem hiding this comment.
I think it's highly desirable to make this a bit more dynamic, in one way or the otherm but I think that can be improved in later PRs.
| const bastionInitialRetryDelay = 30 * time.Second | ||
| const bastionMaxRetryDelay = time.Hour |
There was a problem hiding this comment.
These seem not too unreasonable to me, but it means that if a bastion is consistently down, the witness will exit and need restart about every hour.
There was a problem hiding this comment.
Maybe just keep trying with the max value instead of exiting would make sense?
|
I've added a basic integration-level test. And I wasted an hour or two before realizing the reason for a strange failure was that the testscript package always runs its tests in parallel, and I added a second test that wanted to run a bastion on the default port localhost:8443... |
|
FWIW -- I'm now running this successfully on rgdd.se/poc-witness. Notes:
Only thing that maybe stood out when I was testing:
|
|
Thanks for testing.
With a separate table for bastions, allowing multiple bastions for a log seemed easier than enforcing at most one. And the connection logic doesn't care about the mapping, it gets all the bastions ( When a request is received, the list of bastions for that origin line is looked up, and compared to what's attached to the request context. I could see some use for multiple bastions, either for redundancy, or for migration from one bastion to another. |
|
OK, thanks for explaining how you were thinking when picking direction! SGTM. I just wanted to make sure we weren't inheriting behavior from -bastion cmdline which we might want to deprecate. Keeping the connection open to all the bastions that are specified seems like a clean behavior (rather than picking the first one that works on startup -- the current cmdline behavior). I'll try the "dual bastion" behavior on Monday when I'm migrating back to bastion.glasklar.is. |
|
Some of the things left out of this MR, which could be follow up work:
|
8d209b3 to
f784a5d
Compare
| e <- srv.ListenAndServe() | ||
| }() | ||
| } else if len(logBastions) == 0 { | ||
| fatal("configured to not open a listening port, but no bastions configured") |
There was a problem hiding this comment.
Not sure if this should be a fatal error, or a warning or something configurable.
There was a problem hiding this comment.
Given the explicit use of -no-listen, presumably it's a new witness where logs with bastions will be added later. Changed to a warning.
0d9928f to
c983b2f
Compare
|
Updated testing notes (after a few minor tinkering rounds with nisse):
(Note: I have not done any detailed code review - just blackbox testing. AFAICT this PR does what I want.) (There's the same configuration nit UX here wrt. accepting the same bastion host multiple times as in #41. Does not cause any issues AFAICT, so maybe can fix that minor nit separately for both add-keys and add-bastions later.) @FiloSottile -- can you take a look and ensure we get this merged from here on? Let me know if you want me to help with any more testing (or review) tomorrow. |
FiloSottile
left a comment
There was a problem hiding this comment.
Love how clean this turned out. I pushed fixes for a few issues.
| {"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"]} | ||
| {"origin":"sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/f48d4a1d0c6370ec189dc537f648ef3bb347b012fbd3c899a630a4cd2e9b8702+9bc54c7f+AVv6q3xDaHxI2aTemqEb7W6ZcbO7QbTqTr20thOqfqsw"]} | ||
| {"origin":"sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/fae7fd8f084f9e7a1482162da8a3e52b08e6c1bac74ab831d00eb5c983b84120+7f693d84+AUlxeri80AO7/4j/+OGo+5M2Sud0ktFg34uZl2fZnjJT"]} | ||
| {"origin":"example.com/foo","size":0,"root_hash":"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=","keys":["sigsum.org/v1/tree/2ef59132082631d13e353b5ae49b22bc51a9bd59f41a2d570960a9658c1ed151+e2137795+ATp+37IPHc3SbPGzFMyZmPTOUlClk6PYPH+Ce5JiCb/h"],"bastions":[null]} |
There was a problem hiding this comment.
Fixed with an explicit COALESCE.
| e <- srv.ListenAndServe() | ||
| }() | ||
| } else if len(logBastions) == 0 { | ||
| fatal("configured to not open a listening port, but no bastions configured") |
There was a problem hiding this comment.
Given the explicit use of -no-listen, presumably it's a new witness where logs with bastions will be added later. Changed to a warning.
| if len(bastions) > 0 { | ||
| // Accept requests only via these bastions. | ||
| if bastion == "" { | ||
| l.Debug("rejected request not using bastion") |
There was a problem hiding this comment.
Hmm, I think requests coming to the public listen endpoint (if enabled) should be allowed for all logs. Witnesses that are worried about receiving too much traffic are going to use -no-listen anyway, and rejecting them would make it hard to migrate a log from direct connection to a bastion, because as soon as the witness operator adds the bastion, the requests will start failing until the log switches to using the bastion.
| } | ||
| time.Sleep(delay) | ||
| delay *= 2 | ||
| } |
There was a problem hiding this comment.
I have reworked this logic to
- cap the retry at one minute, exponential backoff is useful to avoid thundering herds, but even 100 witnesses doing 1 minute retries will be 1.7qps, totally manageable; on the other hand, a 1 hour backoff extends an outage by 30 minutes on average
- return if ctx is cancelled, the previous logic would keep trying to reconnect (and fail due to the cancelled ctx) after deleting a bastion
- not kill the process if a per-log bastion is unavailable for a long time, the witness serves multiple logs and one log should not affect the others (this also combined with (2) to kill the process ~2h after a bastion was deleted)
- reset the delay if the connection is alive for a fixed amount of time, not for an amount of time dependent on the latest delay (the two are not related)
- make the delays easier to think about by expanding them into a table
| return fmt.Errorf("connecting to bastion: %v", err) | ||
| } | ||
| // Ensure that the connection is closed when our context is cancelled. | ||
| go func() { |
There was a problem hiding this comment.
The function can return even if the context is not cancelled, leaking the goroutine.
dbafaa1 to
205b357
Compare
|
|
||
| slog.Info("connected to bastion", "bastion", bastion) | ||
| if logSpecific { | ||
| ctx = witness.ContextWithBastion(ctx, bastion) |
There was a problem hiding this comment.
This write to the ctx variable races with using ctx in the goroutine above.
The function can return even if the context is not cancelled, leaking the goroutine.
f911b25 to
94baa7d
Compare
| exec witnessctl add-bastion -origin=sigsum.org/v1/tree/4d6d8825a6bb689d459628312889dfbb0bcd41b5211d9e1ce768b0ff0309e562 -bastion=localhost:9443 | ||
|
|
||
| # start bastion | ||
| exec litebastion -listen localhost:9443 -testcert -backends=backends.txt &litebastion& |
There was a problem hiding this comment.
With your change to make "no bastion configured" into a warning rather than error, this invocation can now use -no-listen.
| // Ensure that the connection is closed when our context is cancelled. | ||
| ctx, cancel = context.WithCancel(ctx) | ||
| defer cancel() | ||
| go func(ctx context.Context) { | ||
| // TODO: gracefully complete in-flight requests. | ||
| <-ctx.Done() | ||
| conn.Close() | ||
| }(ctx) | ||
|
|
||
| slog.Info("connected to bastion", "bastion", bastion) | ||
| if logSpecific { | ||
| ctx = witness.ContextWithBastion(ctx, bastion) |
There was a problem hiding this comment.
Note that it is unclear to me why this is needed; it would make sense to me if one could have ServeConn close the connection when the context passed down to that machinery is cancelled.
I also first tried to move creation of this goroutine later, after the ctx = witness.ContextWithBastion(...) call, but then it seemed to have no effect. It's as if the use of context.WithValue breaks parent/child relation, which is weird.
No description provided.