Skip to content

Commit

Permalink
bootstrap: use path-based SDS resources for xDS cert and key
Browse files Browse the repository at this point in the history
Replaced direct certificate/key path with path-based SDS resources in Envoy
bootstrap configuration. This allows certificate rotation: Envoy will watch and
reload the certificates and key associated to the SDS resources without need
to restart Envoy.

Signed-off-by: Tero Saarni <[email protected]>
  • Loading branch information
tsaarni committed Mar 11, 2020
1 parent cb9575a commit bd7f38f
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 19 deletions.
41 changes: 23 additions & 18 deletions cmd/contour/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
package main

import (
"io"
"log"
"os"
"path"

"github.com/gogo/protobuf/proto"
"github.com/golang/protobuf/jsonpb"
"github.com/projectcontour/contour/internal/envoy"
kingpin "gopkg.in/alecthomas/kingpin.v2"
Expand All @@ -28,7 +30,7 @@ func registerBootstrap(app *kingpin.Application) (*kingpin.CmdClause, *bootstrap
var ctx bootstrapContext

bootstrap := app.Command("bootstrap", "Generate bootstrap configuration.")
bootstrap.Arg("path", "Configuration file ('-' for standard output).").Required().StringVar(&ctx.path)
bootstrap.Arg("path", "Directory where configuration files will be written to.").Required().StringVar(&ctx.config.Path)
bootstrap.Flag("admin-address", "Envoy admin interface address.").StringVar(&ctx.config.AdminAddress)
bootstrap.Flag("admin-port", "Envoy admin interface port.").IntVar(&ctx.config.AdminPort)
bootstrap.Flag("xds-address", "xDS gRPC API address.").StringVar(&ctx.config.XDSAddress)
Expand All @@ -42,28 +44,31 @@ func registerBootstrap(app *kingpin.Application) (*kingpin.CmdClause, *bootstrap

type bootstrapContext struct {
config envoy.BootstrapConfig
path string
}

// doBootstrap writes an Envoy bootstrap configuration file to the supplied path.
// doBootstrap writes an Envoy bootstrap configuration files to the supplied path.
func doBootstrap(ctx *bootstrapContext) {
var out io.Writer

switch ctx.path {
case "-":
out = os.Stdout
default:
f, err := os.Create(ctx.path)
check(err)
if ctx.config.GrpcClientCert != "" || ctx.config.GrpcClientKey != "" || ctx.config.GrpcCABundle != "" {
// If one of the two TLS options is not empty, they all must be not empty
if !(ctx.config.GrpcClientCert != "" && ctx.config.GrpcClientKey != "" && ctx.config.GrpcCABundle != "") {
log.Fatal("You must supply all three TLS parameters - --envoy-cafile, --envoy-cert-file, --envoy-key-file, or none of them.")
}
writeConfig(path.Join(ctx.config.Path, envoy.SdsTLSCertificateFile), envoy.TLSCertificateSdsSecretConfig(ctx.config.GrpcClientCert, ctx.config.GrpcClientKey))
writeConfig(path.Join(ctx.config.Path, envoy.SdsValidationContextFile), envoy.ValidationContextSdsSecretConfig(ctx.config.GrpcCABundle))
}

out = f
writeConfig(path.Join(ctx.config.Path, envoy.BootstrapConfigFile), envoy.Bootstrap(&ctx.config))
}

defer func() {
check(f.Close())
}()
}
func writeConfig(filename string, config proto.Message) {
out, err := os.Create(filename)
check(err)

m := &jsonpb.Marshaler{OrigName: true}
defer func() {
check(out.Close())
}()

check(m.Marshal(out, envoy.Bootstrap(&ctx.config)))
m := &jsonpb.Marshaler{OrigName: true}
check(m.Marshal(out, config))
}
93 changes: 92 additions & 1 deletion internal/envoy/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package envoy

import (
"log"
"path"
"strconv"
"strings"
"time"
Expand All @@ -27,9 +28,21 @@ import (
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
bootstrap "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v2"
matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
"github.com/golang/protobuf/ptypes/any"
"github.com/projectcontour/contour/internal/protobuf"
)

// BootstrapConfigFile stores the path to Envoy's bootstrap configuration.
const BootstrapConfigFile = "envoy.json"

// SdsTLSCertificateFile stores the path to the SDS resource with Envoy's
// client certificate and key for XDS gRPC connection.
const SdsTLSCertificateFile = "envoy-sds-auth-secret-tls-certicate.json"

// SdsValidationContextFile stores the path to the SDS resource with
// CA certificates for Envoy to use for the XDS gRPC connection.
const SdsValidationContextFile = "envoy-sds-auth-secret-validation-context.json"

// Bootstrap creates a new v2 Bootstrap configuration.
func Bootstrap(c *BootstrapConfig) *bootstrap.Bootstrap {
b := &bootstrap.Bootstrap{
Expand Down Expand Up @@ -99,7 +112,7 @@ func Bootstrap(c *BootstrapConfig) *bootstrap.Bootstrap {
log.Fatal("You must supply all three TLS parameters - --envoy-cafile, --envoy-cert-file, --envoy-key-file, or none of them.")
}
b.StaticResources.Clusters[0].TransportSocket = UpstreamTLSTransportSocket(
upstreamFileTLSContext(c.GrpcCABundle, c.GrpcClientCert, c.GrpcClientKey),
upstreamSdsTLSContext(path.Join(c.Path, SdsTLSCertificateFile), path.Join(c.Path, SdsValidationContextFile)),
)
}

Expand Down Expand Up @@ -141,6 +154,81 @@ func upstreamFileTLSContext(cafile, certfile, keyfile string) *envoy_api_v2_auth
return context
}

func upstreamSdsTLSContext(certificateSdsFile, validationSdsFile string) *envoy_api_v2_auth.UpstreamTlsContext {
context := &envoy_api_v2_auth.UpstreamTlsContext{
CommonTlsContext: &envoy_api_v2_auth.CommonTlsContext{
TlsCertificateSdsSecretConfigs: []*envoy_api_v2_auth.SdsSecretConfig{{
SdsConfig: &envoy_api_v2_core.ConfigSource{
ConfigSourceSpecifier: &envoy_api_v2_core.ConfigSource_Path{
Path: certificateSdsFile,
},
},
}},
ValidationContextType: &envoy_api_v2_auth.CommonTlsContext_ValidationContextSdsSecretConfig{
ValidationContextSdsSecretConfig: &envoy_api_v2_auth.SdsSecretConfig{
SdsConfig: &envoy_api_v2_core.ConfigSource{
ConfigSourceSpecifier: &envoy_api_v2_core.ConfigSource_Path{
Path: validationSdsFile,
},
},
},
},
},
}
return context
}

func discoveryResponse(secret *envoy_api_v2_auth.Secret) *api.DiscoveryResponse {
response := &api.DiscoveryResponse{
Resources: []*any.Any{toAny(secret)},
}
return response
}

// TLSCertificateSdsSecretConfig creates DiscoveryResponse with file based SDS resource
// including paths to TLS certificates and key
func TLSCertificateSdsSecretConfig(certfile, keyfile string) *api.DiscoveryResponse {
secret := &envoy_api_v2_auth.Secret{
Type: &envoy_api_v2_auth.Secret_TlsCertificate{
TlsCertificate: &envoy_api_v2_auth.TlsCertificate{
CertificateChain: &envoy_api_v2_core.DataSource{
Specifier: &envoy_api_v2_core.DataSource_Filename{
Filename: certfile,
},
},
PrivateKey: &envoy_api_v2_core.DataSource{
Specifier: &envoy_api_v2_core.DataSource_Filename{
Filename: keyfile,
},
},
},
},
}
return discoveryResponse(secret)
}

// ValidationContextSdsSecretConfig creates DiscoveryResponse with file based SDS resource
// including path to CA certificate bundle
func ValidationContextSdsSecretConfig(cafile string) *api.DiscoveryResponse {
secret := &envoy_api_v2_auth.Secret{
Type: &envoy_api_v2_auth.Secret_ValidationContext{
ValidationContext: &envoy_api_v2_auth.CertificateValidationContext{
TrustedCa: &envoy_api_v2_core.DataSource{
Specifier: &envoy_api_v2_core.DataSource_Filename{
Filename: cafile,
},
},
MatchSubjectAltNames: []*matcher.StringMatcher{{
MatchPattern: &matcher.StringMatcher_Exact{
Exact: "contour",
}},
},
},
},
}
return discoveryResponse(secret)
}

// BootstrapConfig holds configuration values for a v2.Bootstrap.
type BootstrapConfig struct {
// AdminAccessLogPath is the path to write the access log for the administration server.
Expand Down Expand Up @@ -176,6 +264,9 @@ type BootstrapConfig struct {

// GrpcClientKey is the filename that contains a client key for secure gRPC with TLS.
GrpcClientKey string

// Path is the directory where configuration files are created.
Path string
}

func (c *BootstrapConfig) xdsAddress() string { return stringOrDefault(c.XDSAddress, "127.0.0.1") }
Expand Down

0 comments on commit bd7f38f

Please sign in to comment.