Skip to content

Commit 4af1bff

Browse files
niels-mollerNiels MöllerFiloSottile
authored
cmd/litewitness: implement per-log bastions (#35)
Co-authored-by: Niels Möller <nisse@glasklarteknik.se> Co-authored-by: Filippo Valsorda <hi@filippo.io>
1 parent e5e918c commit 4af1bff

7 files changed

Lines changed: 487 additions & 31 deletions

File tree

cmd/litewitness/README.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,17 @@ SSH_AUTH_SOCK=litewitness.sock ssh-add litewitness.pem
5656
address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected
5757
-listen string
5858
address to listen for HTTP requests (default "localhost:7380")
59-
60-
Only one of `-bastion` or `-listen` must be specified. The former will cause
61-
litewitness to serve requests through a bastion reverse proxy (see below). The
62-
latter will listen for HTTP requests on the specified port. (HTTPS needs to be
63-
terminated outside of litewitness.) The bastion flag is an optionally
64-
comma-separated list of bastions to try in order until one connects
65-
successfully. If the connection drops after establishing, litewitness exits.
59+
-no-listen
60+
do not open any listening socket, rely exclusively on bastions
61+
62+
Only one of `-bastion` or `-listen` must be specified, or `-no-listen` can be
63+
used to rely exclusively on per-log bastions configured in the database. The
64+
`-bastion` flag will cause litewitness to serve requests through a bastion
65+
reverse proxy (see below). The `-listen` flag will listen for HTTP requests on
66+
the specified port. (HTTPS needs to be terminated outside of litewitness.) The
67+
bastion flag is an optionally comma-separated list of bastions to try in order
68+
until one connects successfully. If the connection drops after establishing,
69+
litewitness exits.
6670

6771
## witnessctl
6872

@@ -81,6 +85,14 @@ re-added. To disable a log, remove all its keys.
8185
The `add-key` and `del-key` commands add and remove verifier keys for a known
8286
log. The name of the key must match the log origin.
8387

88+
witnessctl add-bastion -db <path> -origin <origin> -bastion <address:port>
89+
witnessctl del-bastion -db <path> -origin <origin> -bastion <address:port>
90+
91+
The `add-bastion` and `del-bastion` commands add and remove bastion addresses
92+
for a log. Multiple bastions can be configured for a log and will be used
93+
simultaneously. Bastion configuration is reloaded when litewitness receives a
94+
SIGHUP signal.
95+
8496
witnessctl add-sigsum-log -db <path> -key <hex-encoded key>
8597

8698
The `add-sigsum-log` command is a helper that adds a new Sigsum log, computing

cmd/litewitness/litewitness.go

Lines changed: 146 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"net/http"
2222
"os"
2323
"os/signal"
24+
"slices"
2425
"strings"
2526
"syscall"
2627
"time"
@@ -39,10 +40,64 @@ var nameFlag = flag.String("name", "", "URL-like (e.g. example.com/foo) name of
3940
var dbFlag = flag.String("db", "litewitness.db", "path to sqlite database")
4041
var sshAgentFlag = flag.String("ssh-agent", "litewitness.sock", "path to ssh-agent socket")
4142
var listenFlag = flag.String("listen", "localhost:7380", "address to listen for HTTP requests")
43+
var noListenFlag = flag.Bool("no-listen", false, "do not open any listening socket, rely exclusively on bastions")
4244
var keyFlag = flag.String("key", "", "SSH fingerprint (with SHA256: prefix) of the witness key")
4345
var bastionFlag = flag.String("bastion", "", "address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected")
4446
var testCertFlag = flag.Bool("testcert", false, "use rootCA.pem for connections to the bastion")
4547

48+
type ConnectionSet struct {
49+
connections map[string]func() // connection => cancel func
50+
connect func(context.Context, string)
51+
}
52+
53+
func NewConnectionSet(connect func(context.Context, string)) *ConnectionSet {
54+
return &ConnectionSet{
55+
connections: make(map[string]func()),
56+
connect: connect,
57+
}
58+
}
59+
60+
func (s *ConnectionSet) Configure(ctx context.Context, addrs []string) {
61+
slices.Sort(addrs)
62+
63+
// Disconnect addresses that have disappeared.
64+
var toDelete []string
65+
for addr, cancel := range s.connections {
66+
if _, found := slices.BinarySearch(addrs, addr); !found {
67+
cancel()
68+
// Postpone delete, we can't delete while iterating over the map.
69+
toDelete = append(toDelete, addr)
70+
}
71+
}
72+
for _, addr := range toDelete {
73+
delete(s.connections, addr)
74+
}
75+
76+
// Connect new bastions.
77+
for _, addr := range addrs {
78+
if _, found := s.connections[addr]; found {
79+
continue
80+
}
81+
// Quit early on cancel.
82+
if ctx.Err() != nil {
83+
break
84+
}
85+
connectionCtx, cancel := context.WithCancel(ctx)
86+
s.connections[addr] = cancel
87+
go s.connect(connectionCtx, addr)
88+
}
89+
}
90+
91+
func onSignal(signo os.Signal, callback func()) {
92+
c := make(chan os.Signal, 1)
93+
signal.Notify(c, signo)
94+
go func() {
95+
for range c {
96+
callback()
97+
}
98+
}()
99+
}
100+
46101
func main() {
47102
flag.Parse()
48103

@@ -52,18 +107,14 @@ func main() {
52107
console.SetFilter(slogconsole.IPAddressFilter)
53108
slog.SetDefault(slog.New(slogconsole.MultiHandler(h, console)))
54109

55-
c := make(chan os.Signal, 1)
56-
signal.Notify(c, syscall.SIGUSR1)
57-
go func() {
58-
for range c {
59-
slog.Info("received USR1 signal, toggling log level")
60-
if level.Level() == slog.LevelDebug {
61-
level.Set(slog.LevelInfo)
62-
} else {
63-
level.Set(slog.LevelDebug)
64-
}
110+
onSignal(syscall.SIGUSR1, func() {
111+
slog.Info("received USR1 signal, toggling log level")
112+
if level.Level() == slog.LevelDebug {
113+
level.Set(slog.LevelInfo)
114+
} else {
115+
level.Set(slog.LevelDebug)
65116
}
66-
}()
117+
})
67118

68119
signer := connectToSSHAgent()
69120

@@ -89,10 +140,77 @@ func main() {
89140
BaseContext: func(net.Listener) context.Context { return ctx },
90141
}
91142
e := make(chan error, 1)
143+
144+
bastionSet := NewConnectionSet(func(ctx context.Context, addr string) {
145+
var delays = []time.Duration{
146+
100 * time.Millisecond,
147+
1 * time.Second, 1 * time.Second, 1 * time.Second,
148+
5 * time.Second, 15 * time.Second, 30 * time.Second,
149+
1 * time.Minute,
150+
}
151+
152+
// If a connection survives for resetRetryDelay, reset the retry delay.
153+
const resetRetryDelay = 5 * time.Minute
154+
155+
retry := 0
156+
for {
157+
startTime := time.Now()
158+
err := connectToBastion(ctx, addr, signer, srv, true)
159+
duration := time.Since(startTime)
160+
slog.Warn("bastion connection failed", "bastion", addr, "duration", duration, "err", err)
161+
162+
// Quit early on cancel.
163+
if ctx.Err() != nil {
164+
return
165+
}
166+
167+
// If the connection lasted long enough, reset the retry delay.
168+
if duration >= resetRetryDelay {
169+
retry = 0
170+
}
171+
172+
// Wait before retrying.
173+
var delay time.Duration
174+
if retry < len(delays) {
175+
delay = delays[retry]
176+
} else {
177+
delay = delays[len(delays)-1]
178+
}
179+
slog.Info("waiting before reconnecting to bastion", "bastion", addr, "delay", delay)
180+
timer := time.NewTimer(delay)
181+
select {
182+
case <-ctx.Done():
183+
timer.Stop()
184+
return
185+
case <-timer.C:
186+
}
187+
retry++
188+
}
189+
})
190+
191+
// Handle log-specific bastions.
192+
logBastions, err := w.AllBastions()
193+
if err != nil {
194+
fatal("failed looking up bastions", "err", err)
195+
}
196+
bastionSet.Configure(ctx, logBastions)
197+
198+
// At this point, ownership of bastionSet belongs with the signal goroutine,
199+
// and must no longer be accessed by main goroutine.
200+
onSignal(syscall.SIGHUP, func() {
201+
slog.Info("received SIGHUP, reconfiguring bastions")
202+
logBastions, err := w.AllBastions()
203+
if err != nil {
204+
slog.Warn("failed looking up bastions", "err", err)
205+
return
206+
}
207+
bastionSet.Configure(ctx, logBastions)
208+
})
209+
92210
if *bastionFlag != "" {
93211
go func() {
94212
for _, bastion := range strings.Split(*bastionFlag, ",") {
95-
err := connectToBastion(ctx, bastion, signer, srv)
213+
err := connectToBastion(ctx, bastion, signer, srv, false)
96214
if err == errBastionDisconnected {
97215
// Connection succeeded and then was interrupted. Restart to
98216
// let the scheduler apply any backoff, and then retry all bastions.
@@ -102,11 +220,13 @@ func main() {
102220
}
103221
e <- errors.New("couldn't connect to any bastion")
104222
}()
105-
} else {
223+
} else if !*noListenFlag {
106224
go func() {
107225
slog.Info("listening", "addr", *listenFlag)
108226
e <- srv.ListenAndServe()
109227
}()
228+
} else if len(logBastions) == 0 {
229+
slog.Warn("configured to not open a listening port, but no bastions configured")
110230
}
111231

112232
select {
@@ -246,7 +366,7 @@ func indexHandler(w *witness.Witness) http.HandlerFunc {
246366

247367
var errBastionDisconnected = errors.New("connection to bastion interrupted")
248368

249-
func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *http.Server) error {
369+
func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *http.Server, logSpecific bool) error {
250370
slog.Info("connecting to bastion", "bastion", bastion)
251371
cert, err := selfSignedCertificate(signer)
252372
if err != nil {
@@ -279,7 +399,19 @@ func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *
279399
slog.Info("connecting to bastion failed", "bastion", bastion, "err", err)
280400
return fmt.Errorf("connecting to bastion: %v", err)
281401
}
402+
// Ensure that the connection is closed when our context is cancelled.
403+
ctx, cancel = context.WithCancel(ctx)
404+
defer cancel()
405+
go func(ctx context.Context) {
406+
// TODO: gracefully complete in-flight requests.
407+
<-ctx.Done()
408+
conn.Close()
409+
}(ctx)
410+
282411
slog.Info("connected to bastion", "bastion", bastion)
412+
if logSpecific {
413+
ctx = witness.ContextWithBastion(ctx, bastion)
414+
}
283415
// TODO: find a way to surface the fatal error, especially since with
284416
// TLS 1.3 it might be that the bastion rejected the client certificate.
285417
(&http2.Server{

0 commit comments

Comments
 (0)