Skip to content

Commit edeb32b

Browse files
FiloSottilergdd
andcommitted
bastion,cmd/litebastion: add option to accept HTTP on localhost only
Closes #38 Co-authored-by: Rasmus Dahlberg <rasmus@rgdd.se>
1 parent c485592 commit edeb32b

3 files changed

Lines changed: 97 additions & 25 deletions

File tree

bastion/bastion.go

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import (
2020
"errors"
2121
"fmt"
2222
"log/slog"
23+
"net"
2324
"net/http"
2425
"net/http/httputil"
26+
"slices"
2527
"strings"
2628
"sync"
2729
"time"
@@ -51,6 +53,7 @@ type Bastion struct {
5153
c *Config
5254
proxy *httputil.ReverseProxy
5355
pool *backendConnectionsPool
56+
tls *tls.Config
5457
}
5558

5659
type keyHash [sha256.Size]byte
@@ -84,27 +87,14 @@ func New(c *Config) (*Bastion, error) {
8487
Transport: b.pool,
8588
ErrorLog: slog.NewLogLogger(b.pool.log.Handler(), slog.LevelDebug),
8689
}
87-
return b, nil
88-
}
89-
90-
// ConfigureServer sets up srv to handle backend connections to the bastion. It
91-
// wraps TLSConfig.GetConfigForClient to intercept backend connections, and sets
92-
// TLSNextProto for the bastion ALPN protocol. The original tls.Config is still
93-
// used for non-bastion backend connections.
94-
//
95-
// Note that since TLSNextProto won't be nil after a call to ConfigureServer,
96-
// the caller might want to call [http2.ConfigureServer] as well.
97-
func (b *Bastion) ConfigureServer(srv *http.Server) error {
98-
if srv.TLSNextProto == nil {
99-
srv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
100-
}
101-
srv.TLSNextProto["bastion/0"] = b.pool.handleBackend
102-
103-
bastionTLSConfig := &tls.Config{
90+
b.tls = &tls.Config{
10491
MinVersion: tls.VersionTLS13,
10592
NextProtos: []string{"bastion/0"},
10693
ClientAuth: tls.RequireAnyClientCert,
10794
VerifyConnection: func(cs tls.ConnectionState) error {
95+
if cs.NegotiatedProtocol != "bastion/0" {
96+
return fmt.Errorf("missing ALPN")
97+
}
10898
h, err := backendHash(cs)
10999
if err != nil {
110100
return err
@@ -116,17 +106,47 @@ func (b *Bastion) ConfigureServer(srv *http.Server) error {
116106
},
117107
GetCertificate: b.c.GetCertificate,
118108
}
109+
return b, nil
110+
}
111+
112+
// HandleBackendConnection handles a new backend connection.
113+
//
114+
// It can be used alternatively to [Bastion.ConfigureServer] to accept backend
115+
// connections on a dedicated listener.
116+
func (b *Bastion) HandleBackendConnection(conn net.Conn) {
117+
tlsConn := tls.Server(conn, b.tls)
118+
if err := tlsConn.Handshake(); err != nil {
119+
b.pool.log.Debug("failed TLS handshake from backend", "err", err, "remote", conn.RemoteAddr())
120+
conn.Close()
121+
return
122+
}
123+
b.pool.Handle(tlsConn)
124+
conn.Close()
125+
}
126+
127+
// ConfigureServer sets up srv to handle backend connections to the bastion. It
128+
// wraps TLSConfig.GetConfigForClient to intercept backend connections, and sets
129+
// TLSNextProto for the bastion ALPN protocol. The original tls.Config is still
130+
// used for non-bastion backend connections.
131+
//
132+
// Note that since TLSNextProto won't be nil after a call to ConfigureServer,
133+
// the caller might want to call [http2.ConfigureServer] as well.
134+
func (b *Bastion) ConfigureServer(srv *http.Server) error {
135+
if srv.TLSNextProto == nil {
136+
srv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
137+
}
138+
srv.TLSNextProto["bastion/0"] = func(_ *http.Server, c *tls.Conn, _ http.Handler) {
139+
b.pool.Handle(c)
140+
}
119141

120142
if srv.TLSConfig == nil {
121143
srv.TLSConfig = &tls.Config{}
122144
}
123145
oldGetConfigForClient := srv.TLSConfig.GetConfigForClient
124146
srv.TLSConfig.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
125-
for _, proto := range chi.SupportedProtos {
126-
if proto == "bastion/0" {
127-
// This is a bastion connection from a backend.
128-
return bastionTLSConfig, nil
129-
}
147+
if slices.Contains(chi.SupportedProtos, "bastion/0") {
148+
// This is a bastion connection from a backend.
149+
return b.tls, nil
130150
}
131151
if oldGetConfigForClient != nil {
132152
return oldGetConfigForClient(chi)
@@ -212,7 +232,7 @@ func (p *backendConnectionsPool) RoundTrip(r *http.Request) (*http.Response, err
212232
return cc.RoundTrip(r)
213233
}
214234

215-
func (p *backendConnectionsPool) handleBackend(hs *http.Server, c *tls.Conn, h http.Handler) {
235+
func (p *backendConnectionsPool) Handle(c *tls.Conn) {
216236
backend, err := backendHash(c.ConnectionState())
217237
if err != nil {
218238
p.log.Info("failed to get backend hash", "err", err)

cmd/litebastion/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ acceptable client/witness key hashes.
2323
-host string
2424
host to obtain ACME certificate for
2525

26+
If you intend to protect backends from unwanted traffic and not forward
27+
arbitrary requests from the internet, you can accept request on localhost.
28+
This is for example useful when running a bastion for your own log.
29+
30+
-listen-http PORT
31+
only accept HTTP requests at http://127.0.0.1:PORT
32+
2633
Since litebastion needs to operate at a lower level than HTTPS on the witness
2734
side, it can't be behind a reverse proxy, and needs to configure its own TLS
2835
certificate. Use the `-cache`, `-email`, and `-host` flags to configure the ACME

cmd/litebastion/litebastion.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"flag"
1919
"fmt"
2020
"log/slog"
21+
"net"
2122
"net/http"
2223
"os"
2324
"os/signal"
@@ -31,9 +32,11 @@ import (
3132
"golang.org/x/crypto/acme"
3233
"golang.org/x/crypto/acme/autocert"
3334
"golang.org/x/net/http2"
35+
"golang.org/x/sync/errgroup"
3436
)
3537

3638
var listenAddr = flag.String("listen", "localhost:8443", "host and port to listen at")
39+
var listenHTTPPort = flag.String("listen-http", "", "localhost port to listen for HTTP requests")
3740
var testCertificates = flag.Bool("testcert", false, "use localhost.pem and localhost-key.pem instead of ACME")
3841
var autocertCache = flag.String("cache", "", "directory to cache ACME certificates at")
3942
var autocertHost = flag.String("host", "", "host to obtain ACME certificate for")
@@ -145,6 +148,50 @@ func main() {
145148
})
146149
}
147150

151+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
152+
defer stop()
153+
serveGroup, ctx := errgroup.WithContext(ctx)
154+
155+
if *listenHTTPPort != "" {
156+
hs := &http.Server{
157+
Addr: net.JoinHostPort("localhost", *listenHTTPPort),
158+
Handler: http.MaxBytesHandler(mux, 10*1024),
159+
ReadTimeout: 5 * time.Second,
160+
WriteTimeout: 5 * time.Second,
161+
}
162+
l, err := net.Listen("tcp", *listenAddr)
163+
if err != nil {
164+
logFatal("failed to listen for backends", "err", err)
165+
}
166+
serveGroup.Go(func() error {
167+
slog.Info("listening for HTTP", "addr", hs.Addr)
168+
return hs.ListenAndServe()
169+
})
170+
serveGroup.Go(func() error {
171+
slog.Info("listening for backends", "addr", *listenAddr)
172+
for {
173+
c, err := l.Accept()
174+
if err != nil {
175+
return err
176+
}
177+
go b.HandleBackendConnection(c)
178+
}
179+
})
180+
serveGroup.Go(func() error {
181+
<-ctx.Done()
182+
slog.Info("shutting down bastion listener")
183+
l.Close()
184+
slog.Info("shutting down HTTP server")
185+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
186+
defer cancel()
187+
hs.Shutdown(ctx)
188+
return nil
189+
})
190+
err = serveGroup.Wait()
191+
slog.Info("exiting", "err", err)
192+
return
193+
}
194+
148195
hs := &http.Server{
149196
Addr: *listenAddr,
150197
Handler: http.MaxBytesHandler(mux, 10*1024),
@@ -163,8 +210,6 @@ func main() {
163210
}
164211

165212
slog.Info("listening", "addr", *listenAddr)
166-
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
167-
defer stop()
168213
e := make(chan error, 1)
169214
go func() { e <- hs.ListenAndServeTLS("", "") }()
170215
select {

0 commit comments

Comments
 (0)