Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TLS support to proxy mode #3783

Merged
merged 3 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,26 @@ tasks:
--test-records-dir=tmp/records
--test-enable-oplog

run-proxy-secured:
desc: "Run FerretDB in diff-proxy mode (TLS, auth required)"
deps: [build-host]
cmds:
- >
bin/ferretdb{{exeExt}}
--listen-addr=''
--listen-tls=:27018
--listen-tls-cert-file=./build/certs/server-cert.pem
--listen-tls-key-file=./build/certs/server-key.pem
--listen-tls-ca-file=./build/certs/rootCA-cert.pem
--proxy-addr=127.0.0.1:47018
--proxy-tls-cert-file=./build/certs/client-cert.pem
--proxy-tls-key-file=./build/certs/client-key.pem
--proxy-tls-ca-file=./build/certs/rootCA-cert.pem
--mode=diff-proxy
--handler=pg
--postgresql-url='postgres://username@127.0.0.1:5433/ferretdb?search_path='
--test-records-dir=tmp/records

lint:
desc: "Run linters"
cmds:
Expand Down
25 changes: 18 additions & 7 deletions cmd/ferretdb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,17 @@ var cli struct {
TLS string `default:"" help:"Listen TLS address."`
TLSCertFile string `default:"" help:"TLS cert file path."`
TLSKeyFile string `default:"" help:"TLS key file path."`
TLSCAFile string `default:"" help:"TLS CA file path." name:"tls-ca-file"`
TLSCaFile string `default:"" help:"TLS CA file path."`
} `embed:"" prefix:"listen-"`

ProxyAddr string `default:"" help:"Proxy address."`
DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."`
Proxy struct {
Addr string `default:"" help:"Proxy address."`
TLSCertFile string `default:"" help:"Proxy TLS cert file path."`
TLSKeyFile string `default:"" help:"Proxy TLS key file path."`
TLSCaFile string `default:"" help:"Proxy TLS CA file path."`
} `embed:"" prefix:"proxy-"`

DebugAddr string `default:"127.0.0.1:8088" help:"Listen address for HTTP handlers for metrics, pprof, etc."`

// see setCLIPlugins
kong.Plugins
Expand Down Expand Up @@ -400,14 +406,19 @@ func run() {
defer closeBackend()

l := clientconn.NewListener(&clientconn.NewListenerOpts{
TCP: cli.Listen.Addr,
Unix: cli.Listen.Unix,
TCP: cli.Listen.Addr,
Unix: cli.Listen.Unix,

TLS: cli.Listen.TLS,
TLSCertFile: cli.Listen.TLSCertFile,
TLSKeyFile: cli.Listen.TLSKeyFile,
TLSCAFile: cli.Listen.TLSCAFile,
TLSCAFile: cli.Listen.TLSCaFile,

ProxyAddr: cli.Proxy.Addr,
ProxyTLSCertFile: cli.Proxy.TLSCertFile,
ProxyTLSKeyFile: cli.Proxy.TLSKeyFile,
ProxyTLSCAFile: cli.Proxy.TLSCaFile,

ProxyAddr: cli.ProxyAddr,
Mode: clientconn.Mode(cli.Mode),
Metrics: metrics,
Handler: h,
Expand Down
5 changes: 3 additions & 2 deletions ferretdb/ferretdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ func New(config *Config) (*FerretDB, error) {
}

l := clientconn.NewListener(&clientconn.NewListenerOpts{
TCP: config.Listener.TCP,
Unix: config.Listener.Unix,
TCP: config.Listener.TCP,
Unix: config.Listener.Unix,

TLS: config.Listener.TLS,
TLSCertFile: config.Listener.TLSCertFile,
TLSKeyFile: config.Listener.TLSKeyFile,
Expand Down
19 changes: 12 additions & 7 deletions internal/clientconn/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,17 @@

// newConnOpts represents newConn options.
type newConnOpts struct {
netConn net.Conn
mode Mode
l *zap.Logger
handler *handler.Handler
connMetrics *connmetrics.ConnMetrics
proxyAddr string
netConn net.Conn
mode Mode
l *zap.Logger
handler *handler.Handler
connMetrics *connmetrics.ConnMetrics

proxyAddr string
proxyTLSCertFile string
proxyTLSKeyFile string
proxyTLSCAFile string

testRecordsDir string // if empty, no records are created
}

Expand All @@ -105,7 +110,7 @@
var p *proxy.Router
if opts.mode != NormalMode {
var err error
if p, err = proxy.New(opts.proxyAddr); err != nil {
if p, err = proxy.New(opts.proxyAddr, opts.proxyTLSCertFile, opts.proxyTLSKeyFile, opts.proxyTLSCAFile); err != nil {

Check warning on line 113 in internal/clientconn/conn.go

View check run for this annotation

Codecov / codecov/patch

internal/clientconn/conn.go#L113

Added line #L113 was not covered by tests
return nil, lazyerrors.Error(err)
}
}
Expand Down
67 changes: 22 additions & 45 deletions internal/clientconn/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ package clientconn
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"math/rand"
"net"
"os"
"runtime/pprof"
"sync"
"time"
Expand All @@ -34,6 +32,7 @@ import (
"github.com/FerretDB/FerretDB/internal/handler"
"github.com/FerretDB/FerretDB/internal/util/ctxutil"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/tlsutil"
"github.com/FerretDB/FerretDB/internal/wire"
)

Expand All @@ -53,14 +52,19 @@ type Listener struct {

// NewListenerOpts represents listener configuration.
type NewListenerOpts struct {
TCP string
Unix string
TCP string
Unix string

TLS string
TLSCertFile string
TLSKeyFile string
TLSCAFile string

ProxyAddr string
ProxyAddr string
ProxyTLSCertFile string
ProxyTLSKeyFile string
ProxyTLSCAFile string

Mode Mode
Metrics *connmetrics.ListenerMetrics
Handler *handler.Handler
Expand Down Expand Up @@ -201,44 +205,12 @@ type setupTLSListenerOpts struct {

// setupTLSListener returns a new TLS listener or and error.
func setupTLSListener(opts *setupTLSListenerOpts) (net.Listener, error) {
if _, err := os.Stat(opts.certFile); err != nil {
return nil, fmt.Errorf("TLS certificate file: %w", err)
}

if _, err := os.Stat(opts.keyFile); err != nil {
return nil, fmt.Errorf("TLS key file: %w", err)
}

cert, err := tls.LoadX509KeyPair(opts.certFile, opts.keyFile)
config, err := tlsutil.Config(opts.certFile, opts.keyFile, opts.caFile)
if err != nil {
return nil, err
}

config := tls.Config{
Certificates: []tls.Certificate{cert},
}

if opts.caFile != "" {
if _, err = os.Stat(opts.caFile); err != nil {
return nil, fmt.Errorf("TLS CA file: %w", err)
}

var rootCA []byte

if rootCA, err = os.ReadFile(opts.caFile); err != nil {
return nil, err
}

roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(rootCA); !ok {
return nil, fmt.Errorf("failed to parse root certificate")
}

config.ClientAuth = tls.RequireAndVerifyClientCert
config.ClientCAs = roots
}

listener, err := tls.Listen("tcp", opts.addr, &config)
listener, err := tls.Listen("tcp", opts.addr, config)
if err != nil {
return nil, lazyerrors.Error(err)
}
Expand Down Expand Up @@ -302,12 +274,17 @@ func acceptLoop(ctx context.Context, listener net.Listener, wg *sync.WaitGroup,
pprof.SetGoroutineLabels(runCtx)

opts := &newConnOpts{
netConn: netConn,
mode: l.Mode,
l: l.Logger.Named("// " + connID + " "), // derive from the original unnamed logger
handler: l.Handler,
connMetrics: l.Metrics.ConnMetrics, // share between all conns
proxyAddr: l.ProxyAddr,
netConn: netConn,
mode: l.Mode,
l: l.Logger.Named("// " + connID + " "), // derive from the original unnamed logger
handler: l.Handler,
connMetrics: l.Metrics.ConnMetrics, // share between all conns

proxyAddr: l.ProxyAddr,
proxyTLSCertFile: l.ProxyTLSCertFile,
proxyTLSKeyFile: l.ProxyTLSKeyFile,
proxyTLSCAFile: l.ProxyTLSCAFile,

testRecordsDir: l.TestRecordsDir,
}

Expand Down
36 changes: 33 additions & 3 deletions internal/handler/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
import (
"bufio"
"context"
"crypto/tls"
"net"

"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/tlsutil"
"github.com/FerretDB/FerretDB/internal/wire"
)

Expand All @@ -31,10 +34,18 @@
}

// New creates a new Router for a service with given address.
func New(addr string) (*Router, error) {
conn, err := net.Dial("tcp", addr)
func New(addr, certFile, keyFile, caFile string) (*Router, error) {
var conn net.Conn
var err error

if certFile != "" {
conn, err = dialTLS(addr, certFile, keyFile, caFile)
} else {
conn, err = net.Dial("tcp", addr)
}

Check warning on line 45 in internal/handler/proxy/proxy.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/proxy/proxy.go#L37-L45

Added lines #L37 - L45 were not covered by tests

if err != nil {
return nil, err
return nil, lazyerrors.Error(err)

Check warning on line 48 in internal/handler/proxy/proxy.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/proxy/proxy.go#L48

Added line #L48 was not covered by tests
}

return &Router{
Expand All @@ -44,6 +55,25 @@
}, nil
}

// dialTLS connects to the given address using TLS.
func dialTLS(addr, certFile, keyFile, caFile string) (net.Conn, error) {
config, err := tlsutil.Config(certFile, keyFile, caFile)
if err != nil {
return nil, err
}

Check warning on line 63 in internal/handler/proxy/proxy.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/proxy/proxy.go#L59-L63

Added lines #L59 - L63 were not covered by tests

conn, err := tls.Dial("tcp", addr, config)
if err != nil {
return nil, lazyerrors.Error(err)
}

Check warning on line 68 in internal/handler/proxy/proxy.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/proxy/proxy.go#L65-L68

Added lines #L65 - L68 were not covered by tests

if err = conn.Handshake(); err != nil {
return nil, lazyerrors.Error(err)
}

Check warning on line 72 in internal/handler/proxy/proxy.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/proxy/proxy.go#L70-L72

Added lines #L70 - L72 were not covered by tests

return conn, nil

Check warning on line 74 in internal/handler/proxy/proxy.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/proxy/proxy.go#L74

Added line #L74 was not covered by tests
}

// Close stops the handler.
func (r *Router) Close() {
r.conn.Close()
Expand Down
66 changes: 66 additions & 0 deletions internal/util/tlsutil/tlsutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package tlsutil provides TLS utilities.
package tlsutil

import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
)

// Config provides TLS configuration for the given certificate and key files.
// If CA file is provided, full authentication is enabled.
func Config(certFile, keyFile, caFile string) (*tls.Config, error) {
if _, err := os.Stat(certFile); err != nil {
return nil, fmt.Errorf("TLS certificate file: %w", err)
}

Check warning on line 30 in internal/util/tlsutil/tlsutil.go

View check run for this annotation

Codecov / codecov/patch

internal/util/tlsutil/tlsutil.go#L29-L30

Added lines #L29 - L30 were not covered by tests

if _, err := os.Stat(keyFile); err != nil {
return nil, fmt.Errorf("TLS key file: %w", err)
}

Check warning on line 34 in internal/util/tlsutil/tlsutil.go

View check run for this annotation

Codecov / codecov/patch

internal/util/tlsutil/tlsutil.go#L33-L34

Added lines #L33 - L34 were not covered by tests

cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("TLS file pair: %w", err)
}

Check warning on line 39 in internal/util/tlsutil/tlsutil.go

View check run for this annotation

Codecov / codecov/patch

internal/util/tlsutil/tlsutil.go#L38-L39

Added lines #L38 - L39 were not covered by tests

config := &tls.Config{
Certificates: []tls.Certificate{cert},
}

if caFile != "" {
if _, err := os.Stat(caFile); err != nil {
return nil, fmt.Errorf("TLS CA file: %w", err)
}

Check warning on line 48 in internal/util/tlsutil/tlsutil.go

View check run for this annotation

Codecov / codecov/patch

internal/util/tlsutil/tlsutil.go#L47-L48

Added lines #L47 - L48 were not covered by tests

b, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}

Check warning on line 53 in internal/util/tlsutil/tlsutil.go

View check run for this annotation

Codecov / codecov/patch

internal/util/tlsutil/tlsutil.go#L52-L53

Added lines #L52 - L53 were not covered by tests

ca := x509.NewCertPool()
if ok := ca.AppendCertsFromPEM(b); !ok {
return nil, fmt.Errorf("TLS CA file: failed to parse")
}

Check warning on line 58 in internal/util/tlsutil/tlsutil.go

View check run for this annotation

Codecov / codecov/patch

internal/util/tlsutil/tlsutil.go#L57-L58

Added lines #L57 - L58 were not covered by tests

config.ClientAuth = tls.RequireAndVerifyClientCert
config.ClientCAs = ca
config.RootCAs = ca
}

return config, nil
}
3 changes: 3 additions & 0 deletions website/docs/configuration/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Some default values are overridden in [our Docker image](../quickstart-guide/doc
| `--listen-tls-key-file` | TLS key file path | `FERRETDB_LISTEN_TLS_KEY_FILE` | |
| `--listen-tls-ca-file` | TLS CA file path | `FERRETDB_LISTEN_TLS_CA_FILE` | |
| `--proxy-addr` | Proxy address | `FERRETDB_PROXY_ADDR` | |
| `--proxy-tls-cert-file` | Proxy TLS cert file path | `FERRETDB_PROXY_TLS_CERT_FILE` | |
| `--proxy-tls-key-file` | Proxy TLS key file path | `FERRETDB_PROXY_TLS_KEY_FILE` | |
| `--proxy-tls-ca-file` | Proxy TLS CA file path | `FERRETDB_PROXY_TLS_CA_FILE` | |
| `--debug-addr` | Listen address for HTTP handlers for metrics, pprof, etc<br />(set to `-` to disable) | `FERRETDB_DEBUG_ADDR` | `127.0.0.1:8088`<br />(`:8088` for Docker) |

## Backend handlers
Expand Down
Loading