Skip to content
Open
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
97 changes: 88 additions & 9 deletions trillian/ctfe/ct_server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import (
"crypto/tls"
"flag"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
Expand Down Expand Up @@ -88,11 +90,20 @@ var (
cacheSize = flag.Int("cache_size", -1, "Size parameter set to 0 makes cache of unlimited size")
cacheTTL = flag.Duration("cache_ttl", -1*time.Second, "Providing 0 TTL turns expiring off")
trillianTLSCACertFile = flag.String("trillian_tls_ca_cert_file", "", "CA certificate file to use for secure connections with Trillian server")
trillianInsecureBackend = flag.Bool("trillian_insecure_backend", false, "Allow plaintext gRPC connections to Trillian backends (unsafe; prefer --trillian_tls_ca_cert_file). If unset, plaintext is only allowed for local backends (loopback/unix).")
maxCertChainSize = flag.Int64("max_cert_chain_size", 512000, "Maximum size of certificate chain in bytes for add-chain and add-pre-chain endpoints (default: 512000 bytes = 500KB)")
)

const unknownRemoteUser = "UNKNOWN_REMOTE"

type trillianTransportMode string

const (
trillianTransportTLS trillianTransportMode = "tls"
trillianTransportPlaintextLocal trillianTransportMode = "plaintext_local"
trillianTransportPlaintextFlag trillianTransportMode = "plaintext_flag"
)

// nolint:staticcheck
func main() {
klog.InitFlags(nil)
Expand Down Expand Up @@ -145,15 +156,6 @@ func main() {
}

dialOpts := []grpc.DialOption{}
if *trillianTLSCACertFile != "" {
creds, err := credentials.NewClientTLSFromFile(*trillianTLSCACertFile, "")
if err != nil {
klog.Exitf("Failed to create TLS credentials from Trillian CA certificate: %v", err)
}
dialOpts = append(dialOpts, grpc.WithTransportCredentials(creds))
} else {
dialOpts = append(dialOpts, grpc.WithInsecure())
}
if len(*etcdServers) > 0 {
// Use etcd to provide endpoint resolution.
cfg := clientv3.Config{Endpoints: strings.Split(*etcdServers, ","), DialTimeout: 5 * time.Second}
Expand Down Expand Up @@ -211,6 +213,28 @@ func main() {
dialOpts = append(dialOpts, grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`))
}

backendSpecs := make([]string, 0, len(beMap))
for _, be := range beMap {
backendSpecs = append(backendSpecs, be.BackendSpec)
}
securityOpt, mode, err := trillianBackendDialOption(*trillianTLSCACertFile, *trillianInsecureBackend, backendSpecs)
if err != nil {
klog.Exitf("%v", err)
}
if securityOpt != nil {
dialOpts = append(dialOpts, securityOpt)
}
switch mode {
case trillianTransportPlaintextLocal:
klog.Warning("Using plaintext gRPC for local Trillian backends (loopback/unix). This is unsafe across untrusted networks; configure --trillian_tls_ca_cert_file to use TLS.")
case trillianTransportPlaintextFlag:
klog.Warning("Using plaintext gRPC for Trillian backends because --trillian_insecure_backend is set. This is unsafe across untrusted networks; prefer --trillian_tls_ca_cert_file.")
case trillianTransportTLS:
// No extra log; TLS is the safe default.
default:
klog.Warningf("Unknown Trillian transport mode: %q", mode)
}

// Dial all our log backends.
clientMap := make(map[string]trillian.TrillianLogClient)
for _, be := range beMap {
Expand Down Expand Up @@ -373,6 +397,61 @@ func main() {
klog.Flush()
}

func trillianBackendDialOption(trillianTLSCACertFile string, allowInsecure bool, backendSpecs []string) (grpc.DialOption, trillianTransportMode, error) {
if trillianTLSCACertFile != "" {
creds, err := credentials.NewClientTLSFromFile(trillianTLSCACertFile, "")
if err != nil {
return nil, "", fmt.Errorf("failed to create TLS credentials from Trillian CA certificate: %w", err)
}
return grpc.WithTransportCredentials(creds), trillianTransportTLS, nil
}

if allowInsecure {
return grpc.WithInsecure(), trillianTransportPlaintextFlag, nil
}

for _, backendSpec := range backendSpecs {
if !isLocalBackendSpec(backendSpec) {
return nil, "", fmt.Errorf("refusing to use plaintext gRPC to non-local Trillian backend %q without --trillian_tls_ca_cert_file (set --trillian_insecure_backend to override)", backendSpec)
}
}
return grpc.WithInsecure(), trillianTransportPlaintextLocal, nil
}

func isLocalBackendSpec(backendSpec string) bool {
// Common local-only schemes.
if strings.HasPrefix(backendSpec, "unix:") || strings.HasPrefix(backendSpec, "unix://") {
return true
}

addr := backendSpec
if strings.Contains(backendSpec, "://") {
if u, err := url.Parse(backendSpec); err == nil {
if u.Scheme == "unix" {
return true
}
if u.Host != "" {
addr = u.Host
} else if u.Path != "" {
addr = strings.TrimPrefix(u.Path, "/")
}
}
}

host := addr
if h, _, err := net.SplitHostPort(addr); err == nil {
host = h
}

if host == "localhost" {
return true
}
if ip := net.ParseIP(host); ip != nil {
return ip.IsLoopback()
}
return false
}

// awaitSignal waits for standard termination signals, then runs the given
// function; it should be run as a separate goroutine.
func awaitSignal(doneFn func()) {
Expand Down
60 changes: 60 additions & 0 deletions trillian/ctfe/ct_server/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import "testing"

func TestIsLocalBackendSpec(t *testing.T) {
tests := []struct {
name string
spec string
want bool
}{
{name: "localhost", spec: "localhost:8090", want: true},
{name: "ipv4_loopback", spec: "127.0.0.1:8090", want: true},
{name: "ipv6_loopback", spec: "[::1]:8090", want: true},
{name: "dns_scheme_localhost", spec: "dns:///localhost:8090", want: true},
{name: "passthrough_scheme_loopback", spec: "passthrough:///127.0.0.1:8090", want: true},
{name: "unix_scheme", spec: "unix:///tmp/trillian.sock", want: true},
{name: "unix_prefix", spec: "unix:/tmp/trillian.sock", want: true},
{name: "private_ipv4", spec: "10.0.0.1:8090", want: false},
{name: "public_hostname", spec: "trillian.example:8090", want: false},
{name: "passthrough_scheme_remote", spec: "passthrough:///10.0.0.1:8090", want: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isLocalBackendSpec(tt.spec); got != tt.want {
t.Fatalf("isLocalBackendSpec(%q)=%v, want %v", tt.spec, got, tt.want)
}
})
}
}

func TestTrillianBackendDialOption_PlaintextPolicy(t *testing.T) {
t.Run("reject_non_local_without_tls_or_flag", func(t *testing.T) {
_, _, err := trillianBackendDialOption("", false, []string{"10.0.0.1:8090"})
if err == nil {
t.Fatalf("expected error, got nil")
}
})

t.Run("allow_local_without_tls_or_flag", func(t *testing.T) {
_, mode, err := trillianBackendDialOption("", false, []string{"localhost:8090"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mode != trillianTransportPlaintextLocal {
t.Fatalf("mode=%q, want %q", mode, trillianTransportPlaintextLocal)
}
})

t.Run("allow_flag_without_tls", func(t *testing.T) {
_, mode, err := trillianBackendDialOption("", true, []string{"10.0.0.1:8090"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mode != trillianTransportPlaintextFlag {
t.Fatalf("mode=%q, want %q", mode, trillianTransportPlaintextFlag)
}
})
}