Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/friendly-queens-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/proxyd': patch
---

Add support for additional SSL certificates in Docker container
8 changes: 8 additions & 0 deletions go/proxyd/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ RUN make proxyd

FROM alpine:3.14.2

COPY ./go/proxyd/entrypoint.sh /bin/entrypoint.sh

RUN apk update && \
apk add ca-certificates && \
chmod +x /bin/entrypoint.sh

EXPOSE 8080

VOLUME /etc/proxyd

COPY --from=builder /app/bin/proxyd /bin/proxyd

ENTRYPOINT ["/bin/entrypoint.sh"]
CMD ["/bin/proxyd", "/etc/proxyd/proxyd.toml"]
3 changes: 3 additions & 0 deletions go/proxyd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ See `metrics.go` for a list of all available metrics.

The metrics port is configurable via the `metrics.port` and `metrics.host` keys in the config.

## Adding Backend SSL Certificates in Docker

The Docker image runs on Alpine Linux. If you get SSL errors when connecting to a backend within Docker, you may need to add additional certificates to Alpine's certificate store. To do this, bind mount the certificate bundle into a file in `/usr/local/share/ca-certificates`. The `entrypoint.sh` script will then update the store with whatever is in the `ca-certificates` directory prior to starting `proxyd`.
12 changes: 12 additions & 0 deletions go/proxyd/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package proxyd
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -127,6 +128,15 @@ func WithMaxWSConns(maxConns int) BackendOpt {
}
}

func WithTLSConfig(tlsConfig *tls.Config) BackendOpt {
return func(b *Backend) {
if b.client.Transport == nil {
b.client.Transport = &http.Transport{}
}
b.client.Transport.(*http.Transport).TLSClientConfig = tlsConfig
}
}

func NewBackend(
name string,
rpcURL string,
Expand Down Expand Up @@ -188,6 +198,7 @@ func (b *Backend) Forward(ctx context.Context, req *RPCReq) (*RPCRes, error) {
RecordRPCError(ctx, b.Name, req.Method, res.Error)
log.Info(
"backend responded with RPC error",
"backend", b.Name,
"code", res.Error.Code,
"msg", res.Error.Message,
"req_id", GetReqID(ctx),
Expand All @@ -196,6 +207,7 @@ func (b *Backend) Forward(ctx context.Context, req *RPCReq) (*RPCRes, error) {
)
} else {
log.Info("forwarded RPC request",
"backend", b.Name,
"method", req.Method,
"auth", GetAuthCtx(ctx),
"req_id", GetReqID(ctx),
Expand Down
5 changes: 4 additions & 1 deletion go/proxyd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ type BackendConfig struct {
RPCURL string `toml:"rpc_url"`
WSURL string `toml:"ws_url"`
MaxRPS int `toml:"max_rps"`
MaxWSConns int `toml:"max_ws_conns"`
MaxWSConns int `toml:"max_ws_conns"`
CAFile string `toml:"ca_file"`
ClientCertFile string `toml:"client_cert_file"`
ClientKeyFile string `toml:"client_key_file"`
}

type BackendsConfig map[string]*BackendConfig
Expand Down
6 changes: 6 additions & 0 deletions go/proxyd/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

echo "Updating CA certificates."
update-ca-certificates
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this step happen at build time to make it reproducible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

update-ca-certificates needs to be run whenever the supplied certificates change. Since we support bind-mounting the certificates at runtime, this needs to happen before executing the proxyd binary.

Copy link
Contributor

Choose a reason for hiding this comment

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

Updating certs from the internet at runtime is asking for trouble.

Can we default to the container CA certs, but allow specifying a CA file at runtime (end drop the entrypoint script)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

update-ca-certificates doesn't update certs from the internet, it just appends all certs in the ca-certificates directory into one big file in /etc/ssl: https://gitlab-test.alpinelinux.org/alpine/ca-certificates/-/blob/master/update-ca.c.

That said, I think it's also useful to specify a CA file at runtime for things like client certificates, so I've added that functionality.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it, I thought that update-ca-certificates called out to the network and downloaded the latest certs from the alpine registry

echo "Running CMD."
exec "$@"
6 changes: 6 additions & 0 deletions go/proxyd/example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ username = ""
password = ""
max_rps = 3
max_ws_conns = 1
# Path to a custom root CA.
ca_file = ""
# Path to a custom client cert file.
client_cert_file = ""
# Path to a custom client key file.
client_key_file = ""

[backends.alchemy]
# The URL to contact the backend at.
Expand Down
30 changes: 30 additions & 0 deletions go/proxyd/proxyd.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package proxyd

import (
"crypto/tls"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/log"
Expand Down Expand Up @@ -75,6 +76,14 @@ func Start(config *Config) error {
if cfg.Password != "" {
opts = append(opts, WithBasicAuth(cfg.Username, cfg.Password))
}
tlsConfig, err := configureBackendTLS(cfg)
if err != nil {
return err
}
if tlsConfig != nil {
log.Info("using custom TLS config for backend", "name", name)
opts = append(opts, WithTLSConfig(tlsConfig))
}
back := NewBackend(name, cfg.RPCURL, cfg.WSURL, lim, opts...)
backendNames = append(backendNames, name)
backendsByName[name] = back
Expand Down Expand Up @@ -168,3 +177,24 @@ func Start(config *Config) error {
func secondsToDuration(seconds int) time.Duration {
return time.Duration(seconds) * time.Second
}

func configureBackendTLS(cfg *BackendConfig) (*tls.Config, error) {
if cfg.CAFile == "" {
return nil, nil
}

tlsConfig, err := CreateTLSClient(cfg.CAFile)
if err != nil {
return nil, err
}

if cfg.ClientCertFile != "" && cfg.ClientKeyFile != "" {
cert, err := ParseKeyPair(cfg.ClientCertFile, cfg.ClientKeyFile)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}

return tlsConfig, nil
}
33 changes: 33 additions & 0 deletions go/proxyd/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package proxyd

import (
"crypto/tls"
"crypto/x509"
"errors"
"io/ioutil"
)

func CreateTLSClient(ca string) (*tls.Config, error) {
pem, err := ioutil.ReadFile(ca)
if err != nil {
return nil, wrapErr(err, "error reading CA")
}

roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(pem)
if !ok {
return nil, errors.New("error parsing TLS client cert")
}

return &tls.Config{
RootCAs: roots,
}, nil
}

func ParseKeyPair(crt, key string) (tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(crt, key)
if err != nil {
return tls.Certificate{}, wrapErr(err, "error loading x509 key pair")
}
return cert, nil
}