Agent Auto Config: Implement Certificate Generation (#8360)

Most of the groundwork was laid in previous PRs between adding the cert-monitor package to extracting the logic of signing certificates out of the connect_ca_endpoint.go code and into a method on the server.

This also refactors the auto-config package a bit to split things out into multiple files.
This commit is contained in:
Matt Keeler 2020-07-28 15:31:48 -04:00 committed by GitHub
parent 3a7b9af518
commit 34034b76f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 4325 additions and 449 deletions

View File

@ -460,6 +460,10 @@ func New(options ...AgentOption) (*Agent, error) {
// loaded any auto-config sources yet.
a.config = config
// create the cache using the rate limiting settings from the config. Note that this means
// that these limits are not reloadable.
a.cache = cache.New(a.config.Cache)
if flat.logger == nil {
logConf := &logging.Config{
LogLevel: config.LogLevel,
@ -525,14 +529,34 @@ func New(options ...AgentOption) (*Agent, error) {
return nil, fmt.Errorf("Failed to setup node ID: %v", err)
}
acOpts := []autoconf.Option{
autoconf.WithDirectRPC(a.connPool),
autoconf.WithTLSConfigurator(a.tlsConfigurator),
autoconf.WithBuilderOpts(flat.builderOpts),
autoconf.WithLogger(a.logger),
autoconf.WithOverrides(flat.overrides...),
// We used to do this in the Start method. However it doesn't need to go
// there any longer. Originally it did because we passed the agent
// delegate to some of the cache registrations. Now we just
// pass the agent itself so its safe to move here.
a.registerCache()
cmConf := new(certmon.Config).
WithCache(a.cache).
WithTLSConfigurator(a.tlsConfigurator).
WithDNSSANs(a.config.AutoConfig.DNSSANs).
WithIPSANs(a.config.AutoConfig.IPSANs).
WithDatacenter(a.config.Datacenter).
WithNodeName(a.config.NodeName).
WithFallback(a.autoConfigFallbackTLS).
WithLogger(a.logger.Named(logging.AutoConfig)).
WithTokens(a.tokens)
acCertMon, err := certmon.New(cmConf)
if err != nil {
return nil, err
}
ac, err := autoconf.New(acOpts...)
acConf := new(autoconf.Config).
WithDirectRPC(a.connPool).
WithBuilderOpts(flat.builderOpts).
WithLogger(a.logger).
WithOverrides(flat.overrides...).
WithCertMonitor(acCertMon)
ac, err := autoconf.New(acConf)
if err != nil {
return nil, err
}
@ -663,9 +687,6 @@ func (a *Agent) Start(ctx context.Context) error {
// regular and on-demand state synchronizations (anti-entropy).
a.sync = ae.NewStateSyncer(a.State, c.AEInterval, a.shutdownCh, a.logger)
// create the cache
a.cache = cache.New(c.Cache)
// create the config for the rpc server/client
consulCfg, err := a.consulConfig()
if err != nil {
@ -714,10 +735,6 @@ func (a *Agent) Start(ctx context.Context) error {
a.State.Delegate = a.delegate
a.State.TriggerSyncChanges = a.sync.SyncChanges.Trigger
// Register the cache. We do this much later so the delegate is
// populated from above.
a.registerCache()
if a.config.AutoEncryptTLS && !a.config.ServerMode {
reply, err := a.autoEncryptInitialCertificate(ctx)
if err != nil {
@ -755,6 +772,9 @@ func (a *Agent) Start(ctx context.Context) error {
a.logger.Info("automatically upgraded to TLS")
}
if err := a.autoConf.Start(&lib.StopChannelContext{StopCh: a.shutdownCh}); err != nil {
return fmt.Errorf("AutoConf failed to start certificate monitor: %w", err)
}
a.serviceManager.Start()
// Load checks/services/metadata.
@ -867,6 +887,10 @@ func (a *Agent) autoEncryptInitialCertificate(ctx context.Context) (*structs.Sig
return client.RequestAutoEncryptCerts(ctx, addrs, a.config.ServerPort, a.tokens.AgentToken(), a.config.AutoEncryptDNSSAN, a.config.AutoEncryptIPSAN)
}
func (a *Agent) autoConfigFallbackTLS(ctx context.Context) (*structs.SignedResponse, error) {
return a.autoConf.FallbackTLS(ctx)
}
func (a *Agent) listenAndServeGRPC() error {
if len(a.config.GRPCAddrs) < 1 {
return nil
@ -1827,6 +1851,16 @@ func (a *Agent) ShutdownAgent() error {
// Stop the watches to avoid any notification/state change during shutdown
a.stopAllWatches()
// this would be cancelled anyways (by the closing of the shutdown ch) but
// this should help them to be stopped more quickly
a.autoConf.Stop()
if a.certMonitor != nil {
// this would be cancelled anyways (by the closing of the shutdown ch)
// but this should help them to be stopped more quickly
a.certMonitor.Stop()
}
// Stop the service manager (must happen before we take the stateLock to avoid deadlock)
if a.serviceManager != nil {
a.serviceManager.Stop()

View File

@ -4645,7 +4645,8 @@ func TestAutoConfig_Integration(t *testing.T) {
// eventually this test should really live with integration tests
// the goal here is to have one test server and another test client
// spin up both agents and allow the server to authorize the auto config
// request and then see the client joined
// request and then see the client joined. Finally we force a CA roots
// update and wait to see that the agents TLS certificate gets updated.
cfgDir := testutil.TempDir(t, "auto-config")
@ -4683,7 +4684,6 @@ func TestAutoConfig_Integration(t *testing.T) {
cert_file = "` + certFile + `"
key_file = "` + keyFile + `"
connect { enabled = true }
auto_encrypt { allow_tls = true }
auto_config {
authorization {
enabled = true
@ -4740,11 +4740,44 @@ func TestAutoConfig_Integration(t *testing.T) {
defer client.Shutdown()
retry.Run(t, func(r *retry.R) {
require.NotNil(r, client.Agent.tlsConfigurator.Cert())
})
// when this is successful we managed to get the gossip key and serf addresses to bind to
// and then connect. Additionally we would have to have certificates or else the
// verify_incoming config on the server would not let it work.
testrpc.WaitForTestAgent(t, client.RPC, "dc1", testrpc.WithToken(TestDefaultMasterToken))
// grab the existing cert
cert1 := client.Agent.tlsConfigurator.Cert()
require.NotNil(t, cert1)
// force a roots rotation by updating the CA config
t.Logf("Forcing roots rotation on the server")
ca := connect.TestCA(t, nil)
req := &structs.CARequest{
Datacenter: "dc1",
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
Config: &structs.CAConfiguration{
Provider: "consul",
Config: map[string]interface{}{
"LeafCertTTL": "1h",
"PrivateKey": ca.SigningKey,
"RootCert": ca.RootCert,
"RotationPeriod": "6h",
"IntermediateCertTTL": "3h",
},
},
}
var reply interface{}
require.NoError(t, srv.RPC("ConnectCA.ConfigurationSet", &req, &reply))
// ensure that a new cert gets generated and pushed into the TLS configurator
retry.Run(t, func(r *retry.R) {
require.NotEqual(r, cert1, client.Agent.tlsConfigurator.Cert())
})
// spot check that we now have an ACL token
require.NotEmpty(t, client.tokens.AgentToken())
}

View File

@ -13,84 +13,38 @@ import (
"time"
"github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/proto/pbautoconf"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/go-discover"
discoverk8s "github.com/hashicorp/go-discover/provider/k8s"
"github.com/hashicorp/go-hclog"
"github.com/golang/protobuf/jsonpb"
)
const (
// autoConfigFileName is the name of the file that the agent auto-config settings are
// stored in within the data directory
autoConfigFileName = "auto-config.json"
dummyTrustDomain = "dummytrustdomain"
)
// DirectRPC is the interface that needs to be satisifed for AutoConfig to be able to perform
// direct RPCs against individual servers. This should not use
type DirectRPC interface {
RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error
}
type options struct {
logger hclog.Logger
directRPC DirectRPC
tlsConfigurator *tlsutil.Configurator
builderOpts config.BuilderOpts
waiter *lib.RetryWaiter
overrides []config.Source
}
// Option represents one point of configurability for the New function
// when creating a new AutoConfig object
type Option func(*options)
// WithLogger will cause the created AutoConfig type to use the provided logger
func WithLogger(logger hclog.Logger) Option {
return func(opt *options) {
opt.logger = logger
var (
pbMarshaler = &jsonpb.Marshaler{
OrigName: false,
EnumsAsInts: false,
Indent: " ",
EmitDefaults: true,
}
}
// WithTLSConfigurator will cause the created AutoConfig type to use the provided configurator
func WithTLSConfigurator(tlsConfigurator *tlsutil.Configurator) Option {
return func(opt *options) {
opt.tlsConfigurator = tlsConfigurator
pbUnmarshaler = &jsonpb.Unmarshaler{
AllowUnknownFields: false,
}
}
// WithConnectionPool will cause the created AutoConfig type to use the provided connection pool
func WithDirectRPC(directRPC DirectRPC) Option {
return func(opt *options) {
opt.directRPC = directRPC
}
}
// WithBuilderOpts will cause the created AutoConfig type to use the provided CLI builderOpts
func WithBuilderOpts(builderOpts config.BuilderOpts) Option {
return func(opt *options) {
opt.builderOpts = builderOpts
}
}
// WithRetryWaiter will cause the created AutoConfig type to use the provided retry waiter
func WithRetryWaiter(waiter *lib.RetryWaiter) Option {
return func(opt *options) {
opt.waiter = waiter
}
}
// WithOverrides is used to provide a config source to append to the tail sources
// during config building. It is really only useful for testing to tune non-user
// configurable tunables to make various tests converge more quickly than they
// could otherwise.
func WithOverrides(overrides ...config.Source) Option {
return func(opt *options) {
opt.overrides = overrides
}
}
)
// AutoConfig is all the state necessary for being able to parse a configuration
// as well as perform the necessary RPCs to perform Agent Auto Configuration.
@ -101,86 +55,52 @@ func WithOverrides(overrides ...config.Source) Option {
// then we will need to add some locking here. I am deferring that for now
// to help ease the review of this already large PR.
type AutoConfig struct {
config *config.RuntimeConfig
builderOpts config.BuilderOpts
logger hclog.Logger
directRPC DirectRPC
tlsConfigurator *tlsutil.Configurator
autoConfigData string
waiter *lib.RetryWaiter
overrides []config.Source
}
func flattenOptions(opts []Option) options {
var flat options
for _, opt := range opts {
opt(&flat)
}
return flat
builderOpts config.BuilderOpts
logger hclog.Logger
directRPC DirectRPC
waiter *lib.RetryWaiter
overrides []config.Source
certMonitor CertMonitor
config *config.RuntimeConfig
autoConfigData string
cancel context.CancelFunc
}
// New creates a new AutoConfig object for providing automatic
// Consul configuration.
func New(options ...Option) (*AutoConfig, error) {
flat := flattenOptions(options)
func New(config *Config) (*AutoConfig, error) {
if config == nil {
return nil, fmt.Errorf("must provide a config struct")
}
if flat.directRPC == nil {
if config.DirectRPC == nil {
return nil, fmt.Errorf("must provide a direct RPC delegate")
}
if flat.tlsConfigurator == nil {
return nil, fmt.Errorf("must provide a TLS configurator")
}
logger := flat.logger
logger := config.Logger
if logger == nil {
logger = hclog.NewNullLogger()
} else {
logger = logger.Named(logging.AutoConfig)
}
waiter := flat.waiter
waiter := config.Waiter
if waiter == nil {
waiter = lib.NewRetryWaiter(1, 0, 10*time.Minute, lib.NewJitterRandomStagger(25))
}
ac := &AutoConfig{
builderOpts: flat.builderOpts,
logger: logger,
directRPC: flat.directRPC,
tlsConfigurator: flat.tlsConfigurator,
waiter: waiter,
overrides: flat.overrides,
builderOpts: config.BuilderOpts,
logger: logger,
directRPC: config.DirectRPC,
waiter: waiter,
overrides: config.Overrides,
certMonitor: config.CertMonitor,
}
return ac, nil
}
// LoadConfig will build the configuration including the extraHead source injected
// after all other defaults but before any user supplied configuration and the overrides
// source injected as the final source in the configuration parsing chain.
func LoadConfig(builderOpts config.BuilderOpts, extraHead config.Source, overrides ...config.Source) (*config.RuntimeConfig, []string, error) {
b, err := config.NewBuilder(builderOpts)
if err != nil {
return nil, nil, err
}
if extraHead.Data != "" {
b.Head = append(b.Head, extraHead)
}
if len(overrides) != 0 {
b.Tail = append(b.Tail, overrides...)
}
cfg, err := b.BuildAndValidate()
if err != nil {
return nil, nil, err
}
return &cfg, b.Warnings, nil
}
// ReadConfig will parse the current configuration and inject any
// auto-config sources if present into the correct place in the parsing chain.
func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) {
@ -219,8 +139,18 @@ func (ac *AutoConfig) restorePersistedAutoConfig() (bool, error) {
content, err := ioutil.ReadFile(path)
if err == nil {
rdr := strings.NewReader(string(content))
var resp pbautoconf.AutoConfigResponse
if err := pbUnmarshaler.Unmarshal(rdr, &resp); err != nil {
return false, fmt.Errorf("failed to decode persisted auto-config data: %w", err)
}
if err := ac.update(&resp); err != nil {
return false, fmt.Errorf("error restoring persisted auto-config response: %w", err)
}
ac.logger.Info("restored persisted configuration", "path", path)
ac.autoConfigData = string(content)
return true, nil
}
@ -305,12 +235,12 @@ func (ac *AutoConfig) introToken() (string, error) {
return token, nil
}
// autoConfigHosts is responsible for taking the list of server addresses and
// serverHosts is responsible for taking the list of server addresses and
// resolving any go-discover provider invocations. It will then return a list
// of hosts. These might be hostnames and is expected that DNS resolution may
// be performed after this function runs. Additionally these may contain ports
// so SplitHostPort could also be necessary.
func (ac *AutoConfig) autoConfigHosts() ([]string, error) {
func (ac *AutoConfig) serverHosts() ([]string, error) {
servers := ac.config.AutoConfig.ServerAddresses
providers := make(map[string]discover.Provider)
@ -388,31 +318,20 @@ func (ac *AutoConfig) resolveHost(hostPort string) []net.TCPAddr {
return addrs
}
// recordAutoConfigReply takes an AutoConfig RPC reply records it with the agent
// recordResponse takes an AutoConfig RPC response records it with the agent
// This will persist the configuration to disk (unless in dev mode running without
// a data dir) and will reload the configuration.
func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse) error {
// overwrite the auto encrypt DNS SANs with the ones specified in the auto_config stanza
if len(ac.config.AutoConfig.DNSSANs) > 0 && reply.Config.AutoEncrypt != nil {
reply.Config.AutoEncrypt.DNSSAN = ac.config.AutoConfig.DNSSANs
}
// overwrite the auto encrypt IP SANs with the ones specified in the auto_config stanza
if len(ac.config.AutoConfig.IPSANs) > 0 && reply.Config.AutoEncrypt != nil {
var ips []string
for _, ip := range ac.config.AutoConfig.IPSANs {
ips = append(ips, ip.String())
}
reply.Config.AutoEncrypt.IPSAN = ips
}
conf, err := json.Marshal(translateConfig(reply.Config))
func (ac *AutoConfig) recordResponse(resp *pbautoconf.AutoConfigResponse) error {
serialized, err := pbMarshaler.MarshalToString(resp)
if err != nil {
return fmt.Errorf("failed to encode auto-config configuration as JSON: %w", err)
return fmt.Errorf("failed to encode auto-config response as JSON: %w", err)
}
ac.autoConfigData = string(conf)
if err := ac.update(resp); err != nil {
return err
}
// now that we know the configuration is generally fine including TLS certs go ahead and persist it to disk.
if ac.config.DataDir == "" {
ac.logger.Debug("not persisting auto-config settings because there is no data directory")
return nil
@ -420,7 +339,7 @@ func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
err = ioutil.WriteFile(path, conf, 0660)
err = ioutil.WriteFile(path, []byte(serialized), 0660)
if err != nil {
return fmt.Errorf("failed to write auto-config configurations: %w", err)
}
@ -435,10 +354,10 @@ func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse
// successful the bool return will be true and the err value will indicate whether we
// successfully recorded the auto config settings (persisted to disk and stored internally
// on the AutoConfig object)
func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context) (bool, error) {
func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context, csr string, key string) (*pbautoconf.AutoConfigResponse, error) {
token, err := ac.introToken()
if err != nil {
return false, err
return nil, err
}
request := pbautoconf.AutoConfigRequest{
@ -446,50 +365,63 @@ func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context) (bool, er
Node: ac.config.NodeName,
Segment: ac.config.SegmentName,
JWT: token,
CSR: csr,
}
var reply pbautoconf.AutoConfigResponse
var resp pbautoconf.AutoConfigResponse
servers, err := ac.autoConfigHosts()
servers, err := ac.serverHosts()
if err != nil {
return false, err
return nil, err
}
for _, s := range servers {
// try each IP to see if we can successfully make the request
for _, addr := range ac.resolveHost(s) {
if ctx.Err() != nil {
return false, ctx.Err()
return nil, ctx.Err()
}
ac.logger.Debug("making AutoConfig.InitialConfiguration RPC", "addr", addr.String())
if err = ac.directRPC.RPC(ac.config.Datacenter, ac.config.NodeName, &addr, "AutoConfig.InitialConfiguration", &request, &reply); err != nil {
if err = ac.directRPC.RPC(ac.config.Datacenter, ac.config.NodeName, &addr, "AutoConfig.InitialConfiguration", &request, &resp); err != nil {
ac.logger.Error("AutoConfig.InitialConfiguration RPC failed", "addr", addr.String(), "error", err)
continue
}
return true, ac.recordAutoConfigReply(&reply)
// update the Certificate with the private key we generated locally
if resp.Certificate != nil {
resp.Certificate.PrivateKeyPEM = key
}
return &resp, nil
}
}
return false, ctx.Err()
return nil, ctx.Err()
}
// getInitialConfiguration implements a loop to retry calls to getInitialConfigurationOnce.
// It uses the RetryWaiter on the AutoConfig object to control how often to attempt
// the initial configuration process. It is also canceallable by cancelling the provided context.
func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) error {
// generate a CSR
csr, key, err := ac.generateCSR()
if err != nil {
return err
}
// this resets the failures so that we will perform immediate request
wait := ac.waiter.Success()
for {
select {
case <-wait:
done, err := ac.getInitialConfigurationOnce(ctx)
if done {
return err
}
if err != nil {
resp, err := ac.getInitialConfigurationOnce(ctx, csr, key)
if resp != nil {
return ac.recordResponse(resp)
} else if err != nil {
ac.logger.Error(err.Error())
} else {
ac.logger.Error("No error returned when fetching the initial auto-configuration but no response was either")
}
wait = ac.waiter.Failed()
case <-ctx.Done():
@ -498,3 +430,164 @@ func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) error {
}
}
}
// generateCSR will generate a CSR for an Agent certificate. This should
// be sent along with the AutoConfig.InitialConfiguration RPC. The generated
// CSR does NOT have a real trust domain as when generating this we do
// not yet have the CA roots. The server will update the trust domain
// for us though.
func (ac *AutoConfig) generateCSR() (csr string, key string, err error) {
// We don't provide the correct host here, because we don't know any
// better at this point. Apart from the domain, we would need the
// ClusterID, which we don't have. This is why we go with
// dummyTrustDomain the first time. Subsequent CSRs will have the
// correct TrustDomain.
id := &connect.SpiffeIDAgent{
// will be replaced
Host: dummyTrustDomain,
Datacenter: ac.config.Datacenter,
Agent: ac.config.NodeName,
}
caConfig, err := ac.config.ConnectCAConfiguration()
if err != nil {
return "", "", fmt.Errorf("Cannot generate CSR: %w", err)
}
conf, err := caConfig.GetCommonConfig()
if err != nil {
return "", "", fmt.Errorf("Failed to load common CA configuration: %w", err)
}
if conf.PrivateKeyType == "" {
conf.PrivateKeyType = connect.DefaultPrivateKeyType
}
if conf.PrivateKeyBits == 0 {
conf.PrivateKeyBits = connect.DefaultPrivateKeyBits
}
// Create a new private key
pk, pkPEM, err := connect.GeneratePrivateKeyWithConfig(conf.PrivateKeyType, conf.PrivateKeyBits)
if err != nil {
return "", "", fmt.Errorf("Failed to generate private key: %w", err)
}
dnsNames := append([]string{"localhost"}, ac.config.AutoConfig.DNSSANs...)
ipAddresses := append([]net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::")}, ac.config.AutoConfig.IPSANs...)
// Create a CSR.
//
// The Common Name includes the dummy trust domain for now but Server will
// override this when it is signed anyway so it's OK.
cn := connect.AgentCN(ac.config.NodeName, dummyTrustDomain)
csr, err = connect.CreateCSR(id, cn, pk, dnsNames, ipAddresses)
if err != nil {
return "", "", err
}
return csr, pkPEM, nil
}
// update will take an AutoConfigResponse and do all things necessary
// to restore those settings. This currently involves updating the
// config data to be used during a call to ReadConfig, updating the
// tls Configurator and prepopulating the cache.
func (ac *AutoConfig) update(resp *pbautoconf.AutoConfigResponse) error {
if err := ac.updateConfigFromResponse(resp); err != nil {
return err
}
if err := ac.updateTLSFromResponse(resp); err != nil {
return err
}
return nil
}
// updateConfigFromResponse is responsible for generating the JSON compatible with the
// agent/config.Config struct
func (ac *AutoConfig) updateConfigFromResponse(resp *pbautoconf.AutoConfigResponse) error {
// here we want to serialize the translated configuration for use in injecting into the normal
// configuration parsing chain.
conf, err := json.Marshal(translateConfig(resp.Config))
if err != nil {
return fmt.Errorf("failed to encode auto-config configuration as JSON: %w", err)
}
ac.autoConfigData = string(conf)
return nil
}
// updateTLSFromResponse will update the TLS certificate and roots in the shared
// TLS configurator.
func (ac *AutoConfig) updateTLSFromResponse(resp *pbautoconf.AutoConfigResponse) error {
if ac.certMonitor == nil {
return nil
}
roots, err := translateCARootsToStructs(resp.CARoots)
if err != nil {
return err
}
cert, err := translateIssuedCertToStructs(resp.Certificate)
if err != nil {
return err
}
update := &structs.SignedResponse{
IssuedCert: *cert,
ConnectCARoots: *roots,
ManualCARoots: resp.ExtraCACertificates,
}
if resp.Config != nil && resp.Config.TLS != nil {
update.VerifyServerHostname = resp.Config.TLS.VerifyServerHostname
}
if err := ac.certMonitor.Update(update); err != nil {
return fmt.Errorf("failed to update the certificate monitor: %w", err)
}
return nil
}
func (ac *AutoConfig) Start(ctx context.Context) error {
if ac.certMonitor == nil {
return nil
}
if !ac.config.AutoConfig.Enabled {
return nil
}
_, err := ac.certMonitor.Start(ctx)
return err
}
func (ac *AutoConfig) Stop() bool {
if ac.certMonitor == nil {
return false
}
if !ac.config.AutoConfig.Enabled {
return false
}
return ac.certMonitor.Stop()
}
func (ac *AutoConfig) FallbackTLS(ctx context.Context) (*structs.SignedResponse, error) {
// generate a CSR
csr, key, err := ac.generateCSR()
if err != nil {
return nil, err
}
resp, err := ac.getInitialConfigurationOnce(ctx, csr, key)
if err != nil {
return nil, err
}
return extractSignedResponse(resp)
}

View File

@ -2,21 +2,23 @@ package autoconf
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gogo/protobuf/types"
"github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/proto/pbautoconf"
"github.com/hashicorp/consul/proto/pbconfig"
"github.com/hashicorp/consul/proto/pbconnect"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/tlsutil"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@ -26,7 +28,17 @@ type mockDirectRPC struct {
}
func (m *mockDirectRPC) RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error {
retValues := m.Called(dc, node, addr, method, args, reply)
var retValues mock.Arguments
if method == "AutoConfig.InitialConfiguration" {
req := args.(*pbautoconf.AutoConfigRequest)
csr := req.CSR
req.CSR = ""
retValues = m.Called(dc, node, addr, method, args, reply)
req.CSR = csr
} else {
retValues = m.Called(dc, node, addr, method, args, reply)
}
switch ret := retValues.Get(0).(type) {
case error:
return ret
@ -38,30 +50,48 @@ func (m *mockDirectRPC) RPC(dc string, node string, addr net.Addr, method string
}
}
type mockCertMonitor struct {
mock.Mock
}
func (m *mockCertMonitor) Start(_ context.Context) (<-chan struct{}, error) {
ret := m.Called()
ch := ret.Get(0).(<-chan struct{})
return ch, ret.Error(1)
}
func (m *mockCertMonitor) Stop() bool {
return m.Called().Bool(0)
}
func (m *mockCertMonitor) Update(resp *structs.SignedResponse) error {
var privKey string
// filter out real certificates as we cannot predict their values
if resp != nil && strings.HasPrefix(resp.IssuedCert.PrivateKeyPEM, "-----BEGIN") {
privKey = resp.IssuedCert.PrivateKeyPEM
resp.IssuedCert.PrivateKeyPEM = ""
}
err := m.Called(resp).Error(0)
if privKey != "" {
resp.IssuedCert.PrivateKeyPEM = privKey
}
return err
}
func TestNew(t *testing.T) {
type testCase struct {
opts []Option
config Config
err string
validate func(t *testing.T, ac *AutoConfig)
}
cases := map[string]testCase{
"no-direct-rpc": {
opts: []Option{
WithTLSConfigurator(&tlsutil.Configurator{}),
},
err: "must provide a direct RPC delegate",
},
"no-tls-configurator": {
opts: []Option{
WithDirectRPC(&mockDirectRPC{}),
},
err: "must provide a TLS configurator",
},
"ok": {
opts: []Option{
WithTLSConfigurator(&tlsutil.Configurator{}),
WithDirectRPC(&mockDirectRPC{}),
config: Config{
DirectRPC: &mockDirectRPC{},
},
validate: func(t *testing.T, ac *AutoConfig) {
t.Helper()
@ -72,7 +102,7 @@ func TestNew(t *testing.T) {
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
ac, err := New(tcase.opts...)
ac, err := New(&tcase.config)
if tcase.err != "" {
testutil.RequireErrorContains(t, err, tcase.err)
} else {
@ -173,7 +203,10 @@ func TestInitialConfiguration_disabled(t *testing.T) {
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
directRPC := mockDirectRPC{}
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
conf := new(Config).
WithBuilderOpts(builderOpts).
WithDirectRPC(&directRPC)
ac, err := New(conf)
require.NoError(t, err)
require.NotNil(t, ac)
@ -208,7 +241,10 @@ func TestInitialConfiguration_cancelled(t *testing.T) {
}
directRPC.On("RPC", "dc1", "autoconf", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300}, "AutoConfig.InitialConfiguration", &expectedRequest, mock.Anything).Return(fmt.Errorf("injected error")).Times(0)
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
conf := new(Config).
WithBuilderOpts(builderOpts).
WithDirectRPC(&directRPC)
ac, err := New(conf)
require.NoError(t, err)
require.NotNil(t, ac)
@ -235,26 +271,97 @@ func TestInitialConfiguration_restored(t *testing.T) {
// persist an auto config response to the data dir where it is expected
persistedFile := filepath.Join(dataDir, autoConfigFileName)
response := &pbconfig.Config{
PrimaryDatacenter: "primary",
response := &pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
PrimaryDatacenter: "primary",
TLS: &pbconfig.TLS{
VerifyServerHostname: true,
},
},
CARoots: &pbconnect.CARoots{
ActiveRootID: "active",
TrustDomain: "trust",
Roots: []*pbconnect.CARoot{
{
ID: "active",
Name: "foo",
SerialNumber: 42,
SigningKeyID: "blarg",
NotBefore: &types.Timestamp{Seconds: 5000, Nanos: 100},
NotAfter: &types.Timestamp{Seconds: 10000, Nanos: 9009},
RootCert: "not an actual cert",
Active: true,
},
},
},
Certificate: &pbconnect.IssuedCert{
SerialNumber: "1234",
CertPEM: "not a cert",
PrivateKeyPEM: "private",
Agent: "foo",
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
ValidAfter: &types.Timestamp{Seconds: 6000},
ValidBefore: &types.Timestamp{Seconds: 7000},
},
ExtraCACertificates: []string{"blarg"},
}
data, err := json.Marshal(translateConfig(response))
data, err := pbMarshaler.MarshalToString(response)
require.NoError(t, err)
require.NoError(t, ioutil.WriteFile(persistedFile, data, 0600))
require.NoError(t, ioutil.WriteFile(persistedFile, []byte(data), 0600))
directRPC := mockDirectRPC{}
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
// setup the mock certificate monitor to ensure that the initial state gets
// updated appropriately during config restoration.
certMon := mockCertMonitor{}
certMon.On("Update", &structs.SignedResponse{
IssuedCert: structs.IssuedCert{
SerialNumber: "1234",
CertPEM: "not a cert",
PrivateKeyPEM: "private",
Agent: "foo",
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
ValidAfter: time.Unix(6000, 0),
ValidBefore: time.Unix(7000, 0),
},
ConnectCARoots: structs.IndexedCARoots{
ActiveRootID: "active",
TrustDomain: "trust",
Roots: []*structs.CARoot{
{
ID: "active",
Name: "foo",
SerialNumber: 42,
SigningKeyID: "blarg",
NotBefore: time.Unix(5000, 100),
NotAfter: time.Unix(10000, 9009),
RootCert: "not an actual cert",
Active: true,
// the decoding process doesn't leave this nil
IntermediateCerts: []string{},
},
},
},
ManualCARoots: []string{"blarg"},
VerifyServerHostname: true,
}).Return(nil).Once()
conf := new(Config).
WithBuilderOpts(builderOpts).
WithDirectRPC(&directRPC).
WithCertMonitor(&certMon)
ac, err := New(conf)
require.NoError(t, err)
require.NotNil(t, ac)
cfg, err := ac.InitialConfiguration(context.Background())
require.NoError(t, err)
require.NoError(t, err, data)
require.NotNil(t, cfg)
require.Equal(t, "primary", cfg.PrimaryDatacenter)
// ensure no RPC was made
directRPC.AssertExpectations(t)
certMon.AssertExpectations(t)
}
func TestInitialConfiguration_success(t *testing.T) {
@ -275,7 +382,36 @@ func TestInitialConfiguration_success(t *testing.T) {
require.True(t, ok)
resp.Config = &pbconfig.Config{
PrimaryDatacenter: "primary",
TLS: &pbconfig.TLS{
VerifyServerHostname: true,
},
}
resp.CARoots = &pbconnect.CARoots{
ActiveRootID: "active",
TrustDomain: "trust",
Roots: []*pbconnect.CARoot{
{
ID: "active",
Name: "foo",
SerialNumber: 42,
SigningKeyID: "blarg",
NotBefore: &types.Timestamp{Seconds: 5000, Nanos: 100},
NotAfter: &types.Timestamp{Seconds: 10000, Nanos: 9009},
RootCert: "not an actual cert",
Active: true,
},
},
}
resp.Certificate = &pbconnect.IssuedCert{
SerialNumber: "1234",
CertPEM: "not a cert",
Agent: "foo",
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
ValidAfter: &types.Timestamp{Seconds: 6000},
ValidBefore: &types.Timestamp{Seconds: 7000},
}
resp.ExtraCACertificates = []string{"blarg"}
}
expectedRequest := pbautoconf.AutoConfigRequest{
@ -293,7 +429,44 @@ func TestInitialConfiguration_success(t *testing.T) {
&expectedRequest,
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
// setup the mock certificate monitor to ensure that the initial state gets
// updated appropriately during config restoration.
certMon := mockCertMonitor{}
certMon.On("Update", &structs.SignedResponse{
IssuedCert: structs.IssuedCert{
SerialNumber: "1234",
CertPEM: "not a cert",
PrivateKeyPEM: "", // the mock
Agent: "foo",
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
ValidAfter: time.Unix(6000, 0),
ValidBefore: time.Unix(7000, 0),
},
ConnectCARoots: structs.IndexedCARoots{
ActiveRootID: "active",
TrustDomain: "trust",
Roots: []*structs.CARoot{
{
ID: "active",
Name: "foo",
SerialNumber: 42,
SigningKeyID: "blarg",
NotBefore: time.Unix(5000, 100),
NotAfter: time.Unix(10000, 9009),
RootCert: "not an actual cert",
Active: true,
},
},
},
ManualCARoots: []string{"blarg"},
VerifyServerHostname: true,
}).Return(nil).Once()
conf := new(Config).
WithBuilderOpts(builderOpts).
WithDirectRPC(&directRPC).
WithCertMonitor(&certMon)
ac, err := New(conf)
require.NoError(t, err)
require.NotNil(t, ac)
@ -307,6 +480,7 @@ func TestInitialConfiguration_success(t *testing.T) {
// ensure no RPC was made
directRPC.AssertExpectations(t)
certMon.AssertExpectations(t)
}
func TestInitialConfiguration_retries(t *testing.T) {
@ -381,7 +555,11 @@ func TestInitialConfiguration_retries(t *testing.T) {
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
waiter := lib.NewRetryWaiter(2, 0, 1*time.Millisecond, nil)
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC), WithRetryWaiter(waiter))
conf := new(Config).
WithBuilderOpts(builderOpts).
WithDirectRPC(&directRPC).
WithRetryWaiter(waiter)
ac, err := New(conf)
require.NoError(t, err)
require.NotNil(t, ac)
@ -396,3 +574,162 @@ func TestInitialConfiguration_retries(t *testing.T) {
// ensure no RPC was made
directRPC.AssertExpectations(t)
}
func TestAutoConfig_StartStop(t *testing.T) {
// currently the only thing running for autoconf is just the cert monitor
// so this test only needs to ensure that the cert monitor is started and
// stopped and not that anything with regards to running the cert monitor
// actually work. Those are tested in the cert-monitor package.
_, configDir, builderOpts := testSetupAutoConf(t)
cfgFile := filepath.Join(configDir, "test.json")
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["198.18.0.1", "198.18.0.2:8398", "198.18.0.3:8399", "127.0.0.1:1234"]}, "verify_outgoing": true
}`), 0600))
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
directRPC := &mockDirectRPC{}
certMon := &mockCertMonitor{}
certMon.On("Start").Return((<-chan struct{})(nil), nil).Once()
certMon.On("Stop").Return(true).Once()
conf := new(Config).
WithBuilderOpts(builderOpts).
WithDirectRPC(directRPC).
WithCertMonitor(certMon)
ac, err := New(conf)
require.NoError(t, err)
require.NotNil(t, ac)
cfg, err := ac.ReadConfig()
require.NoError(t, err)
ac.config = cfg
require.NoError(t, ac.Start(context.Background()))
require.True(t, ac.Stop())
certMon.AssertExpectations(t)
directRPC.AssertExpectations(t)
}
func TestFallBackTLS(t *testing.T) {
_, configDir, builderOpts := testSetupAutoConf(t)
cfgFile := filepath.Join(configDir, "test.json")
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["127.0.0.1:8300"]}, "verify_outgoing": true
}`), 0600))
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
directRPC := mockDirectRPC{}
populateResponse := func(val interface{}) {
resp, ok := val.(*pbautoconf.AutoConfigResponse)
require.True(t, ok)
resp.Config = &pbconfig.Config{
PrimaryDatacenter: "primary",
TLS: &pbconfig.TLS{
VerifyServerHostname: true,
},
}
resp.CARoots = &pbconnect.CARoots{
ActiveRootID: "active",
TrustDomain: "trust",
Roots: []*pbconnect.CARoot{
{
ID: "active",
Name: "foo",
SerialNumber: 42,
SigningKeyID: "blarg",
NotBefore: &types.Timestamp{Seconds: 5000, Nanos: 100},
NotAfter: &types.Timestamp{Seconds: 10000, Nanos: 9009},
RootCert: "not an actual cert",
Active: true,
},
},
}
resp.Certificate = &pbconnect.IssuedCert{
SerialNumber: "1234",
CertPEM: "not a cert",
Agent: "foo",
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
ValidAfter: &types.Timestamp{Seconds: 6000},
ValidBefore: &types.Timestamp{Seconds: 7000},
}
resp.ExtraCACertificates = []string{"blarg"}
}
expectedRequest := pbautoconf.AutoConfigRequest{
Datacenter: "dc1",
Node: "autoconf",
JWT: "blarg",
}
directRPC.On(
"RPC",
"dc1",
"autoconf",
&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300},
"AutoConfig.InitialConfiguration",
&expectedRequest,
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
// setup the mock certificate monitor we don't expect it to be used
// as the FallbackTLS method is mainly used by the certificate monitor
// if for some reason it fails to renew the TLS certificate in time.
certMon := mockCertMonitor{}
conf := new(Config).
WithBuilderOpts(builderOpts).
WithDirectRPC(&directRPC).
WithCertMonitor(&certMon)
ac, err := New(conf)
require.NoError(t, err)
require.NotNil(t, ac)
ac.config, err = ac.ReadConfig()
require.NoError(t, err)
actual, err := ac.FallbackTLS(context.Background())
require.NoError(t, err)
expected := &structs.SignedResponse{
ConnectCARoots: structs.IndexedCARoots{
ActiveRootID: "active",
TrustDomain: "trust",
Roots: []*structs.CARoot{
{
ID: "active",
Name: "foo",
SerialNumber: 42,
SigningKeyID: "blarg",
NotBefore: time.Unix(5000, 100),
NotAfter: time.Unix(10000, 9009),
RootCert: "not an actual cert",
Active: true,
},
},
},
IssuedCert: structs.IssuedCert{
SerialNumber: "1234",
CertPEM: "not a cert",
Agent: "foo",
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
ValidAfter: time.Unix(6000, 0),
ValidBefore: time.Unix(7000, 0),
},
ManualCARoots: []string{"blarg"},
VerifyServerHostname: true,
}
// have to just verify that the private key was put in here but we then
// must zero it out so that the remaining equality check will pass
require.NotEmpty(t, actual.IssuedCert.PrivateKeyPEM)
actual.IssuedCert.PrivateKeyPEM = ""
require.Equal(t, expected, actual)
// ensure no RPC was made
directRPC.AssertExpectations(t)
certMon.AssertExpectations(t)
}

View File

@ -0,0 +1,30 @@
package autoconf
import (
"github.com/hashicorp/consul/agent/config"
)
// LoadConfig will build the configuration including the extraHead source injected
// after all other defaults but before any user supplied configuration and the overrides
// source injected as the final source in the configuration parsing chain.
func LoadConfig(builderOpts config.BuilderOpts, extraHead config.Source, overrides ...config.Source) (*config.RuntimeConfig, []string, error) {
b, err := config.NewBuilder(builderOpts)
if err != nil {
return nil, nil, err
}
if extraHead.Data != "" {
b.Head = append(b.Head, extraHead)
}
if len(overrides) != 0 {
b.Tail = append(b.Tail, overrides...)
}
cfg, err := b.BuildAndValidate()
if err != nil {
return nil, nil, err
}
return &cfg, b.Warnings, nil
}

115
agent/auto-config/config.go Normal file
View File

@ -0,0 +1,115 @@
package autoconf
import (
"context"
"net"
"github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-hclog"
)
// DirectRPC is the interface that needs to be satisifed for AutoConfig to be able to perform
// direct RPCs against individual servers. This will not be used for any ongoing RPCs as once
// the agent gets configured, it can go through the normal RPC means of selecting a available
// server automatically.
type DirectRPC interface {
RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error
}
// CertMonitor is the interface that needs to be satisfied for AutoConfig to be able to
// setup monitoring of the Connect TLS certificate after we first get it.
type CertMonitor interface {
Update(*structs.SignedResponse) error
Start(context.Context) (<-chan struct{}, error)
Stop() bool
}
// Config contains all the tunables for AutoConfig
type Config struct {
// Logger is any logger that should be utilized. If not provided,
// then no logs will be emitted.
Logger hclog.Logger
// DirectRPC is the interface to be used by AutoConfig to make the
// AutoConfig.InitialConfiguration RPCs for generating the bootstrap
// configuration. Setting this field is required.
DirectRPC DirectRPC
// BuilderOpts are any configuration building options that should be
// used when loading the Consul configuration. This is mostly a pass
// through from what the CLI will generate. While this option is
// not strictly required, not setting it will prevent AutoConfig
// from doing anything useful. Enabling AutoConfig requires a
// CLI flag or a config file (also specified by the CLI) flag.
// So without providing the opts its equivalent to using the
// configuration of not specifying anything to the consul agent
// cli.
BuilderOpts config.BuilderOpts
// Waiter is a RetryWaiter to be used during retrieval of the
// initial configuration. When a round of requests fails we will
// wait and eventually make another round of requests (1 round
// is trying the RPC once against each configured server addr). The
// waiting implements some backoff to prevent from retrying these RPCs
// to often. This field is not required and if left unset a waiter will
// be used that has a max wait duration of 10 minutes and a randomized
// jitter of 25% of the wait time. Setting this is mainly useful for
// testing purposes to allow testing out the retrying functionality without
// having the test take minutes/hours to complete.
Waiter *lib.RetryWaiter
// Overrides are a list of configuration sources to append to the tail of
// the config builder. This field is optional and mainly useful for testing
// to override values that would be otherwise not user-settable.
Overrides []config.Source
// CertMonitor is the Connect TLS Certificate Monitor to be used for ongoing
// certificate renewals and connect CA roots updates. This field is not
// strictly required but if not provided the TLS certificates retrieved
// through by the AutoConfig.InitialConfiguration RPC will not be used
// or renewed.
CertMonitor CertMonitor
}
// WithLogger will cause the created AutoConfig type to use the provided logger
func (c *Config) WithLogger(logger hclog.Logger) *Config {
c.Logger = logger
return c
}
// WithConnectionPool will cause the created AutoConfig type to use the provided connection pool
func (c *Config) WithDirectRPC(directRPC DirectRPC) *Config {
c.DirectRPC = directRPC
return c
}
// WithBuilderOpts will cause the created AutoConfig type to use the provided CLI builderOpts
func (c *Config) WithBuilderOpts(builderOpts config.BuilderOpts) *Config {
c.BuilderOpts = builderOpts
return c
}
// WithRetryWaiter will cause the created AutoConfig type to use the provided retry waiter
func (c *Config) WithRetryWaiter(waiter *lib.RetryWaiter) *Config {
c.Waiter = waiter
return c
}
// WithOverrides is used to provide a config source to append to the tail sources
// during config building. It is really only useful for testing to tune non-user
// configurable tunables to make various tests converge more quickly than they
// could otherwise.
func (c *Config) WithOverrides(overrides ...config.Source) *Config {
c.Overrides = overrides
return c
}
// WithCertMonitor is used to provide a certificate monitor to the auto-config.
// This monitor is responsible for renewing the agents TLS certificate and keeping
// the connect CA roots up to date.
func (c *Config) WithCertMonitor(certMonitor CertMonitor) *Config {
c.CertMonitor = certMonitor
return c
}

View File

@ -1,7 +1,14 @@
package autoconf
import (
"fmt"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto"
"github.com/hashicorp/consul/proto/pbautoconf"
"github.com/hashicorp/consul/proto/pbconfig"
"github.com/hashicorp/consul/proto/pbconnect"
"github.com/mitchellh/mapstructure"
)
// translateAgentConfig is meant to take in a proto/pbconfig.Config type
@ -158,3 +165,64 @@ func translateConfig(c *pbconfig.Config) map[string]interface{} {
return out
}
func extractSignedResponse(resp *pbautoconf.AutoConfigResponse) (*structs.SignedResponse, error) {
roots, err := translateCARootsToStructs(resp.CARoots)
if err != nil {
return nil, err
}
cert, err := translateIssuedCertToStructs(resp.Certificate)
if err != nil {
return nil, err
}
out := &structs.SignedResponse{
IssuedCert: *cert,
ConnectCARoots: *roots,
ManualCARoots: resp.ExtraCACertificates,
}
if resp.Config != nil && resp.Config.TLS != nil {
out.VerifyServerHostname = resp.Config.TLS.VerifyServerHostname
}
return out, err
}
// translateCARootsToStructs will create a structs.IndexedCARoots object from the corresponding
// protobuf struct. Those structs are intended to be identical so the conversion just uses
// mapstructure to go from one to the other.
func translateCARootsToStructs(in *pbconnect.CARoots) (*structs.IndexedCARoots, error) {
var out structs.IndexedCARoots
if err := mapstructureTranslateToStructs(in, &out); err != nil {
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
}
return &out, nil
}
// translateIssuedCertToStructs will create a structs.IssuedCert object from the corresponding
// protobuf struct. Those structs are intended to be identical so the conversion just uses
// mapstructure to go from one to the other.
func translateIssuedCertToStructs(in *pbconnect.IssuedCert) (*structs.IssuedCert, error) {
var out structs.IssuedCert
if err := mapstructureTranslateToStructs(in, &out); err != nil {
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
}
return &out, nil
}
func mapstructureTranslateToStructs(in interface{}, out interface{}) error {
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: proto.HookPBTimestampToTime,
Result: out,
})
if err != nil {
return err
}
return decoder.Decode(in)
}

View File

@ -230,6 +230,7 @@ func (m *CertMonitor) Start(ctx context.Context) (<-chan struct{}, error) {
exit := make(chan struct{})
go m.run(ctx, exit)
m.logger.Info("certificate monitor started")
return exit, nil
}

View File

@ -1297,6 +1297,10 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
return err
}
if rt.AutoConfig.Enabled && rt.AutoEncryptTLS {
return fmt.Errorf("both auto_encrypt.tls and auto_config.enabled cannot be set to true.")
}
if err := b.validateAutoConfig(rt); err != nil {
return err
}

View File

@ -1692,10 +1692,6 @@ func (c *RuntimeConfig) ClientAddress() (unixAddr, httpAddr, httpsAddr string) {
}
func (c *RuntimeConfig) ConnectCAConfiguration() (*structs.CAConfiguration, error) {
if !c.ConnectEnabled {
return nil, nil
}
ca := &structs.CAConfiguration{
Provider: "consul",
Config: map[string]interface{}{

View File

@ -3803,6 +3803,35 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
///////////////////////////////////
// Auto Config related tests
{
desc: "auto config and auto encrypt error",
args: []string{
`-data-dir=` + dataDir,
},
hcl: []string{`
auto_config {
enabled = true
intro_token = "blah"
server_addresses = ["198.18.0.1"]
}
auto_encrypt {
tls = true
}
verify_outgoing = true
`},
json: []string{`{
"auto_config": {
"enabled": true,
"intro_token": "blah",
"server_addresses": ["198.18.0.1"]
},
"auto_encrypt": {
"tls": true
},
"verify_outgoing": true
}`},
err: "both auto_encrypt.tls and auto_config.enabled cannot be set to true.",
},
{
desc: "auto config not allowed for servers",
args: []string{
@ -7470,12 +7499,6 @@ func TestConnectCAConfiguration(t *testing.T) {
}
cases := map[string]testCase{
"connect-disabled": {
config: RuntimeConfig{
ConnectEnabled: false,
},
expected: nil,
},
"defaults": {
config: RuntimeConfig{
ConnectEnabled: true,

View File

@ -2,22 +2,31 @@ package consul
import (
"context"
"crypto/x509"
"encoding/base64"
"fmt"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/proto"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib/template"
"github.com/hashicorp/consul/proto/pbautoconf"
config "github.com/hashicorp/consul/proto/pbconfig"
"github.com/hashicorp/consul/proto/pbconfig"
"github.com/hashicorp/consul/proto/pbconnect"
"github.com/hashicorp/consul/tlsutil"
bexpr "github.com/hashicorp/go-bexpr"
"github.com/mitchellh/mapstructure"
)
type AutoConfigOptions struct {
NodeName string
SegmentName string
CSR *x509.CertificateRequest
SpiffeID *connect.SpiffeIDAgent
}
type AutoConfigAuthorizer interface {
@ -73,16 +82,35 @@ func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfig
}
}
return AutoConfigOptions{
opts := AutoConfigOptions{
NodeName: req.Node,
SegmentName: req.Segment,
}, nil
}
if req.CSR != "" {
csr, id, err := parseAutoConfigCSR(req.CSR)
if err != nil {
return AutoConfigOptions{}, err
}
if id.Agent != req.Node {
return AutoConfigOptions{}, fmt.Errorf("Spiffe ID agent name (%s) of the certificate signing request is not for the correct node (%s)", id.Agent, req.Node)
}
opts.CSR = csr
opts.SpiffeID = id
}
return opts, nil
}
type AutoConfigBackend interface {
CreateACLToken(template *structs.ACLToken) (*structs.ACLToken, error)
DatacenterJoinAddresses(segment string) ([]string, error)
ForwardRPC(method string, info structs.RPCInfo, args, reply interface{}) (bool, error)
GetCARoots() (*structs.IndexedCARoots, error)
SignCertificate(csr *x509.CertificateRequest, id connect.CertURI) (*structs.IssuedCert, error)
}
// AutoConfig endpoint is used for cluster auto configuration operations
@ -114,15 +142,51 @@ func NewAutoConfig(conf *Config, tlsConfigurator *tlsutil.Configurator, backend
// made aware of its certificates are populated. This will only work if connect is enabled and
// in some cases only if auto_encrypt is enabled on the servers. This endpoint has the option
// to configure auto_encrypt or potentially in the future to generate the certificates inline.
func (ac *AutoConfig) updateTLSCertificatesInConfig(opts AutoConfigOptions, conf *config.Config) error {
conf.AutoEncrypt = &config.AutoEncrypt{TLS: ac.config.AutoEncryptAllowTLS}
func (ac *AutoConfig) updateTLSCertificatesInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
// nothing to be done as we cannot generate certificates
if !ac.config.ConnectEnabled {
return nil
}
if opts.CSR != nil {
cert, err := ac.backend.SignCertificate(opts.CSR, opts.SpiffeID)
if err != nil {
return fmt.Errorf("Failed to sign CSR: %w", err)
}
// convert to the protobuf form of the issued certificate
pbcert, err := translateIssuedCertToProtobuf(cert)
if err != nil {
return err
}
resp.Certificate = pbcert
}
connectRoots, err := ac.backend.GetCARoots()
if err != nil {
return fmt.Errorf("Failed to lookup the CA roots: %w", err)
}
// convert to the protobuf form of the issued certificate
pbroots, err := translateCARootsToProtobuf(connectRoots)
if err != nil {
return err
}
resp.CARoots = pbroots
// get the non-connect CA certs from the TLS Configurator
if ac.tlsConfigurator != nil {
resp.ExtraCACertificates = ac.tlsConfigurator.ManualCAPems()
}
return nil
}
// updateACLtokensInConfig will configure all of the agents ACL settings and will populate
// the configuration with an agent token usable for all default agent operations.
func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, conf *config.Config) error {
acl := &config.ACL{
func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
acl := &pbconfig.ACL{
Enabled: ac.config.ACLsEnabled,
PolicyTTL: ac.config.ACLPolicyTTL.String(),
RoleTTL: ac.config.ACLRoleTTL.String(),
@ -153,48 +217,48 @@ func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, conf *config.Co
return fmt.Errorf("Failed to generate an ACL token for node %q - %w", opts.NodeName, err)
}
acl.Tokens = &config.ACLTokens{Agent: token.SecretID}
acl.Tokens = &pbconfig.ACLTokens{Agent: token.SecretID}
}
conf.ACL = acl
resp.Config.ACL = acl
return nil
}
// updateJoinAddressesInConfig determines the correct gossip endpoints that clients should
// be connecting to for joining the cluster based on the segment given in the opts parameter.
func (ac *AutoConfig) updateJoinAddressesInConfig(opts AutoConfigOptions, conf *config.Config) error {
func (ac *AutoConfig) updateJoinAddressesInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
joinAddrs, err := ac.backend.DatacenterJoinAddresses(opts.SegmentName)
if err != nil {
return err
}
if conf.Gossip == nil {
conf.Gossip = &config.Gossip{}
if resp.Config.Gossip == nil {
resp.Config.Gossip = &pbconfig.Gossip{}
}
conf.Gossip.RetryJoinLAN = joinAddrs
resp.Config.Gossip.RetryJoinLAN = joinAddrs
return nil
}
// updateGossipEncryptionInConfig will populate the gossip encryption configuration settings
func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *config.Config) error {
func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
// Add gossip encryption settings if there is any key loaded
memberlistConfig := ac.config.SerfLANConfig.MemberlistConfig
if lanKeyring := memberlistConfig.Keyring; lanKeyring != nil {
if conf.Gossip == nil {
conf.Gossip = &config.Gossip{}
if resp.Config.Gossip == nil {
resp.Config.Gossip = &pbconfig.Gossip{}
}
if conf.Gossip.Encryption == nil {
conf.Gossip.Encryption = &config.GossipEncryption{}
if resp.Config.Gossip.Encryption == nil {
resp.Config.Gossip.Encryption = &pbconfig.GossipEncryption{}
}
pk := lanKeyring.GetPrimaryKey()
if len(pk) > 0 {
conf.Gossip.Encryption.Key = base64.StdEncoding.EncodeToString(pk)
resp.Config.Gossip.Encryption.Key = base64.StdEncoding.EncodeToString(pk)
}
conf.Gossip.Encryption.VerifyIncoming = memberlistConfig.GossipVerifyIncoming
conf.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
resp.Config.Gossip.Encryption.VerifyIncoming = memberlistConfig.GossipVerifyIncoming
resp.Config.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
}
return nil
@ -202,44 +266,44 @@ func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *
// updateTLSSettingsInConfig will populate the TLS configuration settings but will not
// populate leaf or ca certficiates.
func (ac *AutoConfig) updateTLSSettingsInConfig(_ AutoConfigOptions, conf *config.Config) error {
func (ac *AutoConfig) updateTLSSettingsInConfig(_ AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
if ac.tlsConfigurator == nil {
// TLS is not enabled?
return nil
}
// add in TLS configuration
if conf.TLS == nil {
conf.TLS = &config.TLS{}
if resp.Config.TLS == nil {
resp.Config.TLS = &pbconfig.TLS{}
}
conf.TLS.VerifyServerHostname = ac.tlsConfigurator.VerifyServerHostname()
resp.Config.TLS.VerifyServerHostname = ac.tlsConfigurator.VerifyServerHostname()
base := ac.tlsConfigurator.Base()
conf.TLS.VerifyOutgoing = base.VerifyOutgoing
conf.TLS.MinVersion = base.TLSMinVersion
conf.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
resp.Config.TLS.VerifyOutgoing = base.VerifyOutgoing
resp.Config.TLS.MinVersion = base.TLSMinVersion
resp.Config.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
var err error
conf.TLS.CipherSuites, err = tlsutil.CipherString(base.CipherSuites)
resp.Config.TLS.CipherSuites, err = tlsutil.CipherString(base.CipherSuites)
return err
}
// baseConfig will populate the configuration with some base settings such as the
// datacenter names, node name etc.
func (ac *AutoConfig) baseConfig(opts AutoConfigOptions, conf *config.Config) error {
func (ac *AutoConfig) baseConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
if opts.NodeName == "" {
return fmt.Errorf("Cannot generate auto config response without a node name")
}
conf.Datacenter = ac.config.Datacenter
conf.PrimaryDatacenter = ac.config.PrimaryDatacenter
conf.NodeName = opts.NodeName
conf.SegmentName = opts.SegmentName
resp.Config.Datacenter = ac.config.Datacenter
resp.Config.PrimaryDatacenter = ac.config.PrimaryDatacenter
resp.Config.NodeName = opts.NodeName
resp.Config.SegmentName = opts.SegmentName
return nil
}
type autoConfigUpdater func(c *AutoConfig, opts AutoConfigOptions, conf *config.Config) error
type autoConfigUpdater func(c *AutoConfig, opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error
var (
// variable holding the list of config updating functions to execute when generating
@ -290,15 +354,71 @@ func (ac *AutoConfig) InitialConfiguration(req *pbautoconf.AutoConfigRequest, re
return err
}
conf := &config.Config{}
resp.Config = &pbconfig.Config{}
// update all the configurations
for _, configFn := range autoConfigUpdaters {
if err := configFn(ac, opts, conf); err != nil {
if err := configFn(ac, opts, resp); err != nil {
return err
}
}
resp.Config = conf
return nil
}
func parseAutoConfigCSR(csr string) (*x509.CertificateRequest, *connect.SpiffeIDAgent, error) {
// Parse the CSR string into the x509 CertificateRequest struct
x509CSR, err := connect.ParseCSR(csr)
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse CSR: %w", err)
}
// ensure that a URI SAN is present
if len(x509CSR.URIs) < 1 {
return nil, nil, fmt.Errorf("CSR didn't include any URI SANs")
}
// Parse the SPIFFE ID
spiffeID, err := connect.ParseCertURI(x509CSR.URIs[0])
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse the SPIFFE URI: %w", err)
}
agentID, isAgent := spiffeID.(*connect.SpiffeIDAgent)
if !isAgent {
return nil, nil, fmt.Errorf("SPIFFE ID is not an Agent ID")
}
return x509CSR, agentID, nil
}
func translateCARootsToProtobuf(in *structs.IndexedCARoots) (*pbconnect.CARoots, error) {
var out pbconnect.CARoots
if err := mapstructureTranslateToProtobuf(in, &out); err != nil {
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
}
return &out, nil
}
func translateIssuedCertToProtobuf(in *structs.IssuedCert) (*pbconnect.IssuedCert, error) {
var out pbconnect.IssuedCert
if err := mapstructureTranslateToProtobuf(in, &out); err != nil {
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
}
return &out, nil
}
func mapstructureTranslateToProtobuf(in interface{}, out interface{}) error {
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: proto.HookTimeToPBTimestamp,
Result: out,
})
if err != nil {
return err
}
return decoder.Decode(in)
}

View File

@ -1,6 +1,7 @@
package consul
import (
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
@ -11,6 +12,7 @@ import (
"testing"
"time"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/consul/proto/pbautoconf"
@ -48,6 +50,18 @@ func (m *mockAutoConfigBackend) ForwardRPC(method string, info structs.RPCInfo,
return ret.Bool(0), ret.Error(1)
}
func (m *mockAutoConfigBackend) GetCARoots() (*structs.IndexedCARoots, error) {
ret := m.Called()
roots, _ := ret.Get(0).(*structs.IndexedCARoots)
return roots, ret.Error(1)
}
func (m *mockAutoConfigBackend) SignCertificate(csr *x509.CertificateRequest, id connect.CertURI) (*structs.IssuedCert, error) {
ret := m.Called(csr, id)
cert, _ := ret.Get(0).(*structs.IssuedCert)
return cert, ret.Error(1)
}
func testJWTStandardClaims() jwt.Claims {
now := time.Now()
@ -79,13 +93,6 @@ func signJWTWithStandardClaims(t *testing.T, privKey string, claims interface{})
// * Each of the individual config generation functions. These can be unit tested separately and should NOT
// require running test servers
func TestAutoConfigInitialConfiguration(t *testing.T) {
type testCase struct {
request pbautoconf.AutoConfigRequest
expected pbautoconf.AutoConfigResponse
patchResponse func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse)
err string
}
gossipKey := make([]byte, 32)
// this is not cryptographic randomness and is not secure but for the sake of this test its all we need.
n, err := rand.Read(gossipKey)
@ -105,85 +112,21 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
_, altpriv, err := oidcauthtest.GenerateKey()
require.NoError(t, err)
cases := map[string]testCase{
"wrong-datacenter": {
request: pbautoconf.AutoConfigRequest{
Datacenter: "no-such-dc",
},
err: `invalid datacenter "no-such-dc" - agent auto configuration cannot target a remote datacenter`,
},
"unverifiable": {
request: pbautoconf.AutoConfigRequest{
Node: "test-node",
// this is signed using an incorrect private key
JWT: signJWTWithStandardClaims(t, altpriv, map[string]interface{}{"consul_node_name": "test-node"}),
},
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
},
"claim-assertion-failed": {
request: pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"wrong_claim": "test-node"}),
},
err: "Permission denied: Failed JWT claim assertion",
},
"good": {
request: pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
},
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
Datacenter: "dc1",
PrimaryDatacenter: "dc1",
NodeName: "test-node",
AutoEncrypt: &pbconfig.AutoEncrypt{
TLS: true,
},
ACL: &pbconfig.ACL{
Enabled: true,
PolicyTTL: "30s",
TokenTTL: "30s",
RoleTTL: "30s",
DisabledTTL: "0s",
DownPolicy: "extend-cache",
DefaultPolicy: "deny",
Tokens: &pbconfig.ACLTokens{
Agent: "patched-secret",
},
},
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
},
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
},
},
},
patchResponse: func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse) {
// we are expecting an ACL token but cannot check anything for equality
// so here we check that it was set and overwrite it
require.NotNil(t, resp.Config)
require.NotNil(t, resp.Config.ACL)
require.NotNil(t, resp.Config.ACL.Tokens)
require.NotEmpty(t, resp.Config.ACL.Tokens.Agent)
resp.Config.ACL.Tokens.Agent = "patched-secret"
// we don't know the expected join address until we start up the test server
joinAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: srv.config.SerfLANConfig.MemberlistConfig.AdvertisePort}
require.NotNil(t, resp.Config.Gossip)
require.Equal(t, []string{joinAddr.String()}, resp.Config.Gossip.RetryJoinLAN)
resp.Config.Gossip.RetryJoinLAN = nil
},
},
// this CSR is what gets sent in the request
csrID := connect.SpiffeIDAgent{
Host: "dummy.trustdomain",
Agent: "test-node",
Datacenter: "dc1",
}
csr, _ := connect.TestCSR(t, &csrID)
altCSRID := connect.SpiffeIDAgent{
Host: "dummy.trustdomain",
Agent: "alt",
Datacenter: "dc1",
}
altCSR, _ := connect.TestCSR(t, &altCSRID)
_, s, _ := testACLServerWithConfig(t, func(c *Config) {
c.Domain = "consul"
@ -248,6 +191,126 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
waitForLeaderEstablishment(t, s)
roots, err := s.GetCARoots()
require.NoError(t, err)
pbroots, err := translateCARootsToProtobuf(roots)
require.NoError(t, err)
joinAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: s.config.SerfLANConfig.MemberlistConfig.AdvertisePort}
// -------------------------------------------------------------------------
// Common test setup is now complete
// -------------------------------------------------------------------------
type testCase struct {
request pbautoconf.AutoConfigRequest
expected pbautoconf.AutoConfigResponse
patchResponse func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse)
err string
}
cases := map[string]testCase{
"wrong-datacenter": {
request: pbautoconf.AutoConfigRequest{
Datacenter: "no-such-dc",
},
err: `invalid datacenter "no-such-dc" - agent auto configuration cannot target a remote datacenter`,
},
"unverifiable": {
request: pbautoconf.AutoConfigRequest{
Node: "test-node",
// this is signed using an incorrect private key
JWT: signJWTWithStandardClaims(t, altpriv, map[string]interface{}{"consul_node_name": "test-node"}),
},
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
},
"claim-assertion-failed": {
request: pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"wrong_claim": "test-node"}),
},
err: "Permission denied: Failed JWT claim assertion",
},
"bad-csr-id": {
request: pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
CSR: altCSR,
},
err: "Spiffe ID agent name (alt) of the certificate signing request is not for the correct node (test-node)",
},
"good": {
request: pbautoconf.AutoConfigRequest{
Node: "test-node",
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
CSR: csr,
},
expected: pbautoconf.AutoConfigResponse{
CARoots: pbroots,
ExtraCACertificates: []string{cacert},
Config: &pbconfig.Config{
Datacenter: "dc1",
PrimaryDatacenter: "dc1",
NodeName: "test-node",
ACL: &pbconfig.ACL{
Enabled: true,
PolicyTTL: "30s",
TokenTTL: "30s",
RoleTTL: "30s",
DisabledTTL: "0s",
DownPolicy: "extend-cache",
DefaultPolicy: "deny",
Tokens: &pbconfig.ACLTokens{
Agent: "patched-secret",
},
},
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
RetryJoinLAN: []string{joinAddr.String()},
},
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
},
},
},
patchResponse: func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse) {
// we are expecting an ACL token but cannot check anything for equality
// so here we check that it was set and overwrite it
require.NotNil(t, resp.Config)
require.NotNil(t, resp.Config.ACL)
require.NotNil(t, resp.Config.ACL.Tokens)
require.NotEmpty(t, resp.Config.ACL.Tokens.Agent)
resp.Config.ACL.Tokens.Agent = "patched-secret"
require.NotNil(t, resp.Certificate)
require.NotEmpty(t, resp.Certificate.SerialNumber)
require.NotEmpty(t, resp.Certificate.CertPEM)
require.Empty(t, resp.Certificate.Service)
require.Empty(t, resp.Certificate.ServiceURI)
require.Equal(t, "test-node", resp.Certificate.Agent)
expectedID := connect.SpiffeIDAgent{
Host: roots.TrustDomain,
Agent: "test-node",
Datacenter: "dc1",
}
require.Equal(t, expectedID.URI().String(), resp.Certificate.AgentURI)
// nil this out so we don't check it for equality
resp.Certificate = nil
},
},
}
for testName, tcase := range cases {
t.Run(testName, func(t *testing.T) {
var reply pbautoconf.AutoConfigResponse
@ -269,7 +332,7 @@ func TestAutoConfig_baseConfig(t *testing.T) {
type testCase struct {
serverConfig Config
opts AutoConfigOptions
expected pbconfig.Config
expected pbautoconf.AutoConfigResponse
err string
}
@ -283,11 +346,13 @@ func TestAutoConfig_baseConfig(t *testing.T) {
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
},
expected: pbconfig.Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
Datacenter: "oSWzfhnU",
PrimaryDatacenter: "53XO9mx4",
NodeName: "lBdc0lsH",
SegmentName: "HZiwlWpi",
},
},
},
"no-node-name": {
@ -305,7 +370,7 @@ func TestAutoConfig_baseConfig(t *testing.T) {
config: &tcase.serverConfig,
}
var actual pbconfig.Config
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.baseConfig(tcase.opts, &actual)
if tcase.err == "" {
require.NoError(t, err)
@ -317,6 +382,13 @@ func TestAutoConfig_baseConfig(t *testing.T) {
}
}
func parseCiphers(t *testing.T, cipherStr string) []uint16 {
t.Helper()
ciphers, err := tlsutil.ParseCiphers(cipherStr)
require.NoError(t, err)
return ciphers
}
func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
@ -328,16 +400,9 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
parseCiphers := func(t *testing.T, cipherStr string) []uint16 {
t.Helper()
ciphers, err := tlsutil.ParseCiphers(cipherStr)
require.NoError(t, err)
return ciphers
}
type testCase struct {
tlsConfig tlsutil.Config
expected pbconfig.Config
expected pbautoconf.AutoConfigResponse
}
cases := map[string]testCase{
@ -350,13 +415,15 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
expected: pbconfig.Config{
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
CipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: true,
MinVersion: "tls12",
PreferServerCipherSuites: true,
CipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
},
@ -369,13 +436,15 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
expected: pbconfig.Config{
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: false,
MinVersion: "tls10",
PreferServerCipherSuites: false,
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
TLS: &pbconfig.TLS{
VerifyOutgoing: true,
VerifyServerHostname: false,
MinVersion: "tls10",
PreferServerCipherSuites: false,
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
},
},
},
},
@ -391,7 +460,7 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
tlsConfigurator: configurator,
}
var actual pbconfig.Config
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err = ac.updateTLSSettingsInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
@ -402,7 +471,7 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
type testCase struct {
conf memberlist.Config
expected pbconfig.Config
expected pbautoconf.AutoConfigResponse
}
gossipKey := make([]byte, 32)
@ -422,12 +491,14 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
GossipVerifyIncoming: true,
GossipVerifyOutgoing: true,
},
expected: pbconfig.Config{
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: true,
VerifyOutgoing: true,
},
},
},
},
@ -438,12 +509,14 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
GossipVerifyIncoming: false,
GossipVerifyOutgoing: false,
},
expected: pbconfig.Config{
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: false,
VerifyOutgoing: false,
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
Gossip: &pbconfig.Gossip{
Encryption: &pbconfig.GossipEncryption{
Key: gossipKeyEncoded,
VerifyIncoming: false,
VerifyOutgoing: false,
},
},
},
},
@ -451,6 +524,9 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
"encryption-disabled": {
// zero values all around - if no keyring is configured then the gossip
// encryption settings should not be set.
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{},
},
},
}
@ -463,7 +539,7 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
config: cfg,
}
var actual pbconfig.Config
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.updateGossipEncryptionInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
@ -472,40 +548,149 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
}
func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
now := time.Now()
later := now.Add(time.Hour)
// Generate a Test CA
ca := connect.TestCA(t, nil)
// roots will be returned by the mock backend
roots := structs.IndexedCARoots{
ActiveRootID: ca.ID,
TrustDomain: connect.TestClusterID + ".consul",
Roots: []*structs.CARoot{
ca,
},
}
// this CSR is what gets put into the opts for the
// function to look at an process
csrID := connect.SpiffeIDAgent{
Host: roots.TrustDomain,
Agent: "test",
Datacenter: "dc1",
}
csrStr, _ := connect.TestCSR(t, &csrID)
csr, err := connect.ParseCSR(csrStr)
require.NoError(t, err)
// fake certificate response for the backend
fakeCert := structs.IssuedCert{
SerialNumber: "1",
CertPEM: "not-currently-decoded",
ValidAfter: now,
ValidBefore: later,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
RaftIndex: structs.RaftIndex{
ModifyIndex: 10,
CreateIndex: 10,
},
}
// translate the fake cert to the protobuf equivalent
// for embedding in expected results
pbcert, err := translateIssuedCertToProtobuf(&fakeCert)
require.NoError(t, err)
// generate a CA certificate to use for specifying non-Connect
// certificates which come back differently in the response
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
require.NoError(t, err)
// write out that ca cert to disk - it is unfortunate that
// this is necessary but creation of the tlsutil.Configurator
// will error if it cannot load the CA certificate from disk
dir := testutil.TempDir(t, "auto-config-tls-certificate")
t.Cleanup(func() { os.RemoveAll(dir) })
cafile := path.Join(dir, "cacert.pem")
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
require.NoError(t, err)
// translate the roots response to protobuf to be embedded
// into the expected results
pbroots, err := translateCARootsToProtobuf(&roots)
require.NoError(t, err)
type testCase struct {
serverConfig Config
expected pbconfig.Config
tlsConfig tlsutil.Config
opts AutoConfigOptions
expected pbautoconf.AutoConfigResponse
}
cases := map[string]testCase{
"auto_encrypt-enabled": {
"no-csr": {
serverConfig: Config{
ConnectEnabled: true,
AutoEncryptAllowTLS: true,
ConnectEnabled: true,
},
expected: pbconfig.Config{
AutoEncrypt: &pbconfig.AutoEncrypt{TLS: true},
tlsConfig: tlsutil.Config{
VerifyOutgoing: true,
VerifyServerHostname: true,
TLSMinVersion: "tls12",
PreferServerCipherSuites: true,
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
expected: pbautoconf.AutoConfigResponse{
CARoots: pbroots,
ExtraCACertificates: []string{cacert},
Config: &pbconfig.Config{},
},
},
"auto_encrypt-disabled": {
"signed-certificate": {
serverConfig: Config{
ConnectEnabled: true,
AutoEncryptAllowTLS: false,
ConnectEnabled: true,
},
expected: pbconfig.Config{
AutoEncrypt: &pbconfig.AutoEncrypt{TLS: false},
tlsConfig: tlsutil.Config{
VerifyOutgoing: true,
VerifyServerHostname: true,
TLSMinVersion: "tls12",
PreferServerCipherSuites: true,
CAFile: cafile,
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
},
opts: AutoConfigOptions{
NodeName: "test",
CSR: csr,
SpiffeID: &csrID,
},
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{},
CARoots: pbroots,
ExtraCACertificates: []string{cacert},
Certificate: pbcert,
},
},
"connect-disabled": {
serverConfig: Config{
ConnectEnabled: false,
},
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
backend := &mockAutoConfigBackend{}
backend.On("GetCARoots").Return(&roots, nil)
backend.On("SignCertificate", tcase.opts.CSR, tcase.opts.SpiffeID).Return(&fakeCert, nil)
tlsConfigurator, err := tlsutil.NewConfigurator(tcase.tlsConfig, testutil.Logger(t))
require.NoError(t, err)
ac := AutoConfig{
config: &tcase.serverConfig,
config: &tcase.serverConfig,
tlsConfigurator: tlsConfigurator,
backend: backend,
}
var actual pbconfig.Config
err := ac.updateTLSCertificatesInConfig(AutoConfigOptions{}, &actual)
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err = ac.updateTLSCertificatesInConfig(tcase.opts, &actual)
require.NoError(t, err)
require.Equal(t, tcase.expected, actual)
})
@ -515,7 +700,7 @@ func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
func TestAutoConfig_updateACLsInConfig(t *testing.T) {
type testCase struct {
config Config
expected pbconfig.Config
expected pbautoconf.AutoConfigResponse
expectACLToken bool
err error
}
@ -542,18 +727,20 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
ACLEnableKeyListPolicy: true,
},
expectACLToken: true,
expected: pbconfig.Config{
ACL: &pbconfig.ACL{
Enabled: true,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
Tokens: &pbconfig.ACLTokens{
Agent: tokenSecret,
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
ACL: &pbconfig.ACL{
Enabled: true,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
Tokens: &pbconfig.ACLTokens{
Agent: tokenSecret,
},
},
},
},
@ -572,16 +759,18 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
ACLEnableKeyListPolicy: true,
},
expectACLToken: false,
expected: pbconfig.Config{
ACL: &pbconfig.ACL{
Enabled: false,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
expected: pbautoconf.AutoConfigResponse{
Config: &pbconfig.Config{
ACL: &pbconfig.ACL{
Enabled: false,
PolicyTTL: "7s",
RoleTTL: "10s",
TokenTTL: "12s",
DisabledTTL: "31s",
DownPolicy: "deny",
DefaultPolicy: "allow",
EnableKeyListPolicy: true,
},
},
},
},
@ -630,7 +819,7 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
ac := AutoConfig{config: &tcase.config, backend: backend}
var actual pbconfig.Config
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.updateACLsInConfig(AutoConfigOptions{NodeName: "something"}, &actual)
if tcase.err != nil {
testutil.RequireErrorContains(t, err, tcase.err.Error())
@ -651,12 +840,12 @@ func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
ac := AutoConfig{backend: backend}
var actual pbconfig.Config
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
err := ac.updateJoinAddressesInConfig(AutoConfigOptions{}, &actual)
require.NoError(t, err)
require.NotNil(t, actual.Gossip)
require.ElementsMatch(t, addrs, actual.Gossip.RetryJoinLAN)
require.NotNil(t, actual.Config.Gossip)
require.ElementsMatch(t, addrs, actual.Config.Gossip.RetryJoinLAN)
backend.AssertExpectations(t)
}

View File

@ -1,6 +1,7 @@
package consul
import (
"context"
"errors"
"fmt"
"io"
@ -523,8 +524,8 @@ func NewServerWithOptions(config *Config, options ...ConsulOption) (*Server, err
return nil, fmt.Errorf("Failed to start Raft: %v", err)
}
if s.config.ConnectEnabled && s.config.AutoEncryptAllowTLS {
go s.trackAutoEncryptCARoots()
if s.config.ConnectEnabled && (s.config.AutoEncryptAllowTLS || s.config.AutoConfigAuthzEnabled) {
go s.connectCARootsMonitor(&lib.StopChannelContext{StopCh: s.shutdownCh})
}
if s.gatewayLocator != nil {
@ -632,18 +633,11 @@ func NewServerWithOptions(config *Config, options ...ConsulOption) (*Server, err
return s, nil
}
func (s *Server) trackAutoEncryptCARoots() {
func (s *Server) connectCARootsMonitor(ctx context.Context) {
for {
select {
case <-s.shutdownCh:
s.logger.Debug("shutting down trackAutoEncryptCARoots because shutdown")
return
default:
}
ws := memdb.NewWatchSet()
state := s.fsm.State()
ws.Add(state.AbandonCh())
ws.Add(s.shutdownCh)
_, cas, err := state.CARoots(ws)
if err != nil {
s.logger.Error("Failed to watch AutoEncrypt CARoot", "error", err)
@ -656,7 +650,11 @@ func (s *Server) trackAutoEncryptCARoots() {
if err := s.tlsConfigurator.UpdateAutoEncryptCA(caPems); err != nil {
s.logger.Error("Failed to update AutoEncrypt CAPems", "error", err)
}
ws.Watch(nil)
if err := ws.WatchCtx(ctx); err == context.Canceled {
s.logger.Info("shutting down Connect CA roots monitor")
return
}
}
}

View File

@ -7,6 +7,7 @@ import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
pbconfig "github.com/hashicorp/consul/proto/pbconfig"
pbconnect "github.com/hashicorp/consul/proto/pbconnect"
io "io"
math "math"
)
@ -39,7 +40,10 @@ type AutoConfigRequest struct {
JWT string `protobuf:"bytes,5,opt,name=JWT,proto3" json:"JWT,omitempty"`
// ConsulToken is a Consul ACL token that the agent requesting the
// configuration already has.
ConsulToken string `protobuf:"bytes,6,opt,name=ConsulToken,proto3" json:"ConsulToken,omitempty"`
ConsulToken string `protobuf:"bytes,6,opt,name=ConsulToken,proto3" json:"ConsulToken,omitempty"`
// CSR is a certificate signing request to be used when generating the
// agents TLS certificate
CSR string `protobuf:"bytes,7,opt,name=CSR,proto3" json:"CSR,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -113,12 +117,27 @@ func (m *AutoConfigRequest) GetConsulToken() string {
return ""
}
func (m *AutoConfigRequest) GetCSR() string {
if m != nil {
return m.CSR
}
return ""
}
// AutoConfigResponse is the data structure sent in response to a AutoConfig.InitialConfiguration request
type AutoConfigResponse struct {
Config *pbconfig.Config `protobuf:"bytes,1,opt,name=Config,proto3" json:"Config,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
// Config is the partial Consul configuration to inject into the agents own configuration
Config *pbconfig.Config `protobuf:"bytes,1,opt,name=Config,proto3" json:"Config,omitempty"`
// CARoots is the current list of Connect CA Roots
CARoots *pbconnect.CARoots `protobuf:"bytes,2,opt,name=CARoots,proto3" json:"CARoots,omitempty"`
// Certificate is the TLS certificate issued for the agent
Certificate *pbconnect.IssuedCert `protobuf:"bytes,3,opt,name=Certificate,proto3" json:"Certificate,omitempty"`
// ExtraCACertificates holds non-Connect certificates that may be necessary
// to verify TLS connections with the Consul servers
ExtraCACertificates []string `protobuf:"bytes,4,rep,name=ExtraCACertificates,proto3" json:"ExtraCACertificates,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AutoConfigResponse) Reset() { *m = AutoConfigResponse{} }
@ -161,31 +180,60 @@ func (m *AutoConfigResponse) GetConfig() *pbconfig.Config {
return nil
}
func (m *AutoConfigResponse) GetCARoots() *pbconnect.CARoots {
if m != nil {
return m.CARoots
}
return nil
}
func (m *AutoConfigResponse) GetCertificate() *pbconnect.IssuedCert {
if m != nil {
return m.Certificate
}
return nil
}
func (m *AutoConfigResponse) GetExtraCACertificates() []string {
if m != nil {
return m.ExtraCACertificates
}
return nil
}
func init() {
proto.RegisterType((*AutoConfigRequest)(nil), "autoconf.AutoConfigRequest")
proto.RegisterType((*AutoConfigResponse)(nil), "autoconf.AutoConfigResponse")
}
func init() { proto.RegisterFile("proto/pbautoconf/auto_config.proto", fileDescriptor_ccc5af992e5daf69) }
func init() {
proto.RegisterFile("proto/pbautoconf/auto_config.proto", fileDescriptor_ccc5af992e5daf69)
}
var fileDescriptor_ccc5af992e5daf69 = []byte{
// 256 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x2a, 0x28, 0xca, 0x2f,
0xc9, 0xd7, 0x2f, 0x48, 0x4a, 0x2c, 0x2d, 0xc9, 0x4f, 0xce, 0xcf, 0x4b, 0xd3, 0x07, 0x31, 0xe2,
0x41, 0xac, 0xcc, 0x74, 0x3d, 0xb0, 0xa4, 0x10, 0x07, 0x4c, 0x4e, 0x4a, 0x1a, 0xa6, 0x1a, 0x22,
0xaf, 0x8f, 0xac, 0x4c, 0x69, 0x2a, 0x23, 0x97, 0xa0, 0x63, 0x69, 0x49, 0xbe, 0x33, 0x58, 0x30,
0x28, 0xb5, 0xb0, 0x34, 0xb5, 0xb8, 0x44, 0x48, 0x8e, 0x8b, 0xcb, 0x25, 0xb1, 0x24, 0x31, 0x39,
0x35, 0xaf, 0x24, 0xb5, 0x48, 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x08, 0x49, 0x44, 0x48, 0x88,
0x8b, 0xc5, 0x2f, 0x3f, 0x25, 0x55, 0x82, 0x09, 0x2c, 0x03, 0x66, 0x0b, 0x49, 0x70, 0xb1, 0x07,
0xa7, 0xa6, 0xe7, 0xa6, 0xe6, 0x95, 0x48, 0xb0, 0x80, 0x85, 0x61, 0x5c, 0x21, 0x01, 0x2e, 0x66,
0xaf, 0xf0, 0x10, 0x09, 0x56, 0xb0, 0x28, 0x88, 0x29, 0xa4, 0xc0, 0xc5, 0xed, 0x9c, 0x9f, 0x57,
0x5c, 0x9a, 0x13, 0x92, 0x9f, 0x9d, 0x9a, 0x27, 0xc1, 0x06, 0x96, 0x41, 0x16, 0x52, 0xb2, 0xe1,
0x12, 0x42, 0x76, 0x56, 0x71, 0x41, 0x7e, 0x5e, 0x71, 0xaa, 0x90, 0x1a, 0x17, 0x1b, 0x44, 0x04,
0xec, 0x26, 0x6e, 0x23, 0x3e, 0x3d, 0xa8, 0x67, 0xa0, 0xea, 0xa0, 0xb2, 0x4e, 0x76, 0x27, 0x1e,
0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x8c, 0xc7, 0x72, 0x0c, 0x51,
0x3a, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xfa, 0x19, 0x89, 0xc5, 0x19,
0x99, 0xc9, 0xf9, 0x45, 0x05, 0xa0, 0xa0, 0x28, 0x2e, 0xcd, 0xd1, 0x47, 0x0f, 0xce, 0x24, 0x36,
0xb0, 0x88, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x46, 0x7e, 0xde, 0xed, 0x69, 0x01, 0x00, 0x00,
// 347 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x92, 0xc1, 0x4a, 0xc3, 0x30,
0x18, 0xc7, 0x8d, 0x9b, 0x9b, 0x4b, 0x41, 0x66, 0x76, 0x09, 0x13, 0xcb, 0xd8, 0x41, 0x86, 0x48,
0x2b, 0x13, 0xaf, 0xc2, 0x8c, 0x1e, 0xf4, 0xe0, 0x21, 0x1b, 0x08, 0x5e, 0xa4, 0xeb, 0xb2, 0xad,
0xb8, 0x25, 0xb5, 0xf9, 0x02, 0x3e, 0x8a, 0x6f, 0xe0, 0xab, 0x78, 0xd4, 0x37, 0x90, 0xf9, 0x22,
0xd2, 0xb4, 0x95, 0x20, 0x9e, 0xfa, 0xef, 0xff, 0xf7, 0xfb, 0xe0, 0x6b, 0x13, 0xdc, 0x4f, 0x33,
0x05, 0x2a, 0x4c, 0xa7, 0x91, 0x01, 0x15, 0x2b, 0x39, 0x0f, 0xf3, 0xf0, 0x98, 0xa7, 0x64, 0x11,
0x58, 0x48, 0x76, 0x2b, 0xd6, 0x3d, 0xa8, 0xec, 0x82, 0x87, 0xae, 0xd6, 0x3d, 0x74, 0xa0, 0x14,
0x31, 0x84, 0xe5, 0xb3, 0xc0, 0xfd, 0x37, 0x84, 0xf7, 0x47, 0x06, 0x14, 0xb3, 0x33, 0x5c, 0x3c,
0x1b, 0xa1, 0x81, 0xf8, 0x18, 0x5f, 0x45, 0x10, 0xc5, 0x42, 0x82, 0xc8, 0x28, 0xea, 0xa1, 0x41,
0x8b, 0x3b, 0x0d, 0x21, 0xb8, 0x7e, 0xa7, 0x66, 0x82, 0x6e, 0x5b, 0x62, 0x33, 0xa1, 0xb8, 0x39,
0x16, 0x8b, 0xb5, 0x90, 0x40, 0xeb, 0xb6, 0xae, 0x5e, 0x49, 0x1b, 0xd7, 0x6e, 0xef, 0x27, 0x74,
0xc7, 0xb6, 0x79, 0x24, 0x3d, 0xec, 0x31, 0x25, 0xb5, 0x59, 0x4d, 0xd4, 0x93, 0x90, 0xb4, 0x61,
0x89, 0x5b, 0xe5, 0x33, 0x6c, 0xcc, 0x69, 0xb3, 0x98, 0x61, 0x63, 0xde, 0xff, 0x44, 0x98, 0xb8,
0x9b, 0xea, 0x54, 0x49, 0x2d, 0xc8, 0x11, 0x6e, 0x14, 0x8d, 0x5d, 0xd3, 0x1b, 0xee, 0x05, 0xe5,
0xe7, 0x97, 0x5e, 0x49, 0xc9, 0x31, 0x6e, 0xb2, 0x11, 0x57, 0x0a, 0xb4, 0xdd, 0xda, 0x1b, 0xb6,
0x83, 0xea, 0x4f, 0x94, 0x3d, 0xaf, 0x04, 0x72, 0x8e, 0x3d, 0x26, 0x32, 0x48, 0xe6, 0x49, 0x1c,
0x81, 0xa0, 0x35, 0xeb, 0x77, 0x7e, 0xfd, 0x1b, 0xad, 0x8d, 0x98, 0xe5, 0x06, 0x77, 0x3d, 0x72,
0x8a, 0x3b, 0xd7, 0x2f, 0x90, 0x45, 0x6c, 0xe4, 0xb4, 0x9a, 0xd6, 0x7b, 0xb5, 0x41, 0x8b, 0xff,
0x87, 0x2e, 0x2f, 0xde, 0x37, 0x3e, 0xfa, 0xd8, 0xf8, 0xe8, 0x6b, 0xe3, 0xa3, 0xd7, 0x6f, 0x7f,
0xeb, 0xe1, 0x64, 0x91, 0xc0, 0xd2, 0x4c, 0x83, 0x58, 0xad, 0xc3, 0x65, 0xa4, 0x97, 0x49, 0xac,
0xb2, 0x34, 0x3f, 0x33, 0x6d, 0x56, 0xe1, 0xdf, 0x5b, 0x31, 0x6d, 0xd8, 0xe6, 0xec, 0x27, 0x00,
0x00, 0xff, 0xff, 0xe2, 0x1d, 0x6e, 0x48, 0x30, 0x02, 0x00, 0x00,
}
func (m *AutoConfigRequest) Marshal() (dAtA []byte, err error) {
@ -233,6 +281,12 @@ func (m *AutoConfigRequest) MarshalTo(dAtA []byte) (int, error) {
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.ConsulToken)))
i += copy(dAtA[i:], m.ConsulToken)
}
if len(m.CSR) > 0 {
dAtA[i] = 0x3a
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.CSR)))
i += copy(dAtA[i:], m.CSR)
}
if m.XXX_unrecognized != nil {
i += copy(dAtA[i:], m.XXX_unrecognized)
}
@ -264,6 +318,41 @@ func (m *AutoConfigResponse) MarshalTo(dAtA []byte) (int, error) {
}
i += n1
}
if m.CARoots != nil {
dAtA[i] = 0x12
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(m.CARoots.Size()))
n2, err := m.CARoots.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n2
}
if m.Certificate != nil {
dAtA[i] = 0x1a
i++
i = encodeVarintAutoConfig(dAtA, i, uint64(m.Certificate.Size()))
n3, err := m.Certificate.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n3
}
if len(m.ExtraCACertificates) > 0 {
for _, s := range m.ExtraCACertificates {
dAtA[i] = 0x22
i++
l = len(s)
for l >= 1<<7 {
dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
l >>= 7
i++
}
dAtA[i] = uint8(l)
i++
i += copy(dAtA[i:], s)
}
}
if m.XXX_unrecognized != nil {
i += copy(dAtA[i:], m.XXX_unrecognized)
}
@ -305,6 +394,10 @@ func (m *AutoConfigRequest) Size() (n int) {
if l > 0 {
n += 1 + l + sovAutoConfig(uint64(l))
}
l = len(m.CSR)
if l > 0 {
n += 1 + l + sovAutoConfig(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -321,6 +414,20 @@ func (m *AutoConfigResponse) Size() (n int) {
l = m.Config.Size()
n += 1 + l + sovAutoConfig(uint64(l))
}
if m.CARoots != nil {
l = m.CARoots.Size()
n += 1 + l + sovAutoConfig(uint64(l))
}
if m.Certificate != nil {
l = m.Certificate.Size()
n += 1 + l + sovAutoConfig(uint64(l))
}
if len(m.ExtraCACertificates) > 0 {
for _, s := range m.ExtraCACertificates {
l = len(s)
n += 1 + l + sovAutoConfig(uint64(l))
}
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@ -529,6 +636,38 @@ func (m *AutoConfigRequest) Unmarshal(dAtA []byte) error {
}
m.ConsulToken = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 7:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field CSR", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.CSR = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipAutoConfig(dAtA[iNdEx:])
@ -619,6 +758,110 @@ func (m *AutoConfigResponse) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field CARoots", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.CARoots == nil {
m.CARoots = &pbconnect.CARoots{}
}
if err := m.CARoots.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Certificate", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Certificate == nil {
m.Certificate = &pbconnect.IssuedCert{}
}
if err := m.Certificate.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ExtraCACertificates", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAutoConfig
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthAutoConfig
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthAutoConfig
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.ExtraCACertificates = append(m.ExtraCACertificates, string(dAtA[iNdEx:postIndex]))
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipAutoConfig(dAtA[iNdEx:])

View File

@ -5,6 +5,7 @@ package autoconf;
option go_package = "github.com/hashicorp/consul/proto/pbautoconf";
import "proto/pbconfig/config.proto";
import "proto/pbconnect/connect.proto";
// AutoConfigRequest is the data structure to be sent along with the
// AutoConfig.InitialConfiguration RPC
@ -28,9 +29,23 @@ message AutoConfigRequest {
// ConsulToken is a Consul ACL token that the agent requesting the
// configuration already has.
string ConsulToken = 6;
// CSR is a certificate signing request to be used when generating the
// agents TLS certificate
string CSR = 7;
}
// AutoConfigResponse is the data structure sent in response to a AutoConfig.InitialConfiguration request
message AutoConfigResponse {
// Config is the partial Consul configuration to inject into the agents own configuration
config.Config Config = 1;
// CARoots is the current list of Connect CA Roots
connect.CARoots CARoots = 2;
// Certificate is the TLS certificate issued for the agent
connect.IssuedCert Certificate = 3;
// ExtraCACertificates holds non-Connect certificates that may be necessary
// to verify TLS connections with the Consul servers
repeated string ExtraCACertificates = 4;
}

View File

@ -0,0 +1,38 @@
// Code generated by protoc-gen-go-binary. DO NOT EDIT.
// source: proto/pbconnect/connect.proto
package pbconnect
import (
"github.com/golang/protobuf/proto"
)
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *CARoots) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *CARoots) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *CARoot) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *CARoot) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *IssuedCert) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *IssuedCert) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,147 @@
syntax = "proto3";
package connect;
option go_package = "github.com/hashicorp/consul/proto/pbconnect";
import "google/protobuf/timestamp.proto";
import "proto/pbcommon/common.proto";
import "proto/pbcommon/common_oss.proto";
// CARoots is the list of all currently trusted CA Roots.
message CARoots {
// ActiveRootID is the ID of a root in Roots that is the active CA root.
// Other roots are still valid if they're in the Roots list but are in
// the process of being rotated out.
string ActiveRootID = 1;
// TrustDomain is the identification root for this Consul cluster. All
// certificates signed by the cluster's CA must have their identifying URI in
// this domain.
//
// This does not include the protocol (currently spiffe://) since we may
// implement other protocols in future with equivalent semantics. It should be
// compared against the "authority" section of a URI (i.e. host:port).
//
// We need to support migrating a cluster between trust domains to support
// Multi-DC migration in Enterprise. In this case the current trust domain is
// here but entries in Roots may also have ExternalTrustDomain set to a
// non-empty value implying they were previous roots that are still trusted
// but under a different trust domain.
//
// Note that we DON'T validate trust domain during AuthZ since it causes
// issues of loss of connectivity during migration between trust domains. The
// only time the additional validation adds value is where the cluster shares
// an external root (e.g. organization-wide root) with another distinct Consul
// cluster or PKI system. In this case, x509 Name Constraints can be added to
// enforce that Consul's CA can only validly sign or trust certs within the
// same trust-domain. Name constraints as enforced by TLS handshake also allow
// seamless rotation between trust domains thanks to cross-signing.
string TrustDomain = 2;
// Roots is a list of root CA certs to trust.
repeated CARoot Roots = 3;
// QueryMeta here is mainly used to contain the latest Raft Index that could
// be used to perform a blocking query.
common.QueryMeta QueryMeta = 4;
}
message CARoot {
// ID is a globally unique ID (UUID) representing this CA root.
string ID = 1;
// Name is a human-friendly name for this CA root. This value is
// opaque to Consul and is not used for anything internally.
string Name = 2;
// SerialNumber is the x509 serial number of the certificate.
uint64 SerialNumber = 3;
// SigningKeyID is the ID of the public key that corresponds to the private
// key used to sign leaf certificates. Is is the HexString format of the
// raw AuthorityKeyID bytes.
string SigningKeyID = 4;
// ExternalTrustDomain is the trust domain this root was generated under. It
// is usually empty implying "the current cluster trust-domain". It is set
// only in the case that a cluster changes trust domain and then all old roots
// that are still trusted have the old trust domain set here.
//
// We currently DON'T validate these trust domains explicitly anywhere, see
// IndexedRoots.TrustDomain doc. We retain this information for debugging and
// future flexibility.
string ExternalTrustDomain = 5;
// Time validity bounds.
google.protobuf.Timestamp NotBefore = 6;
google.protobuf.Timestamp NotAfter = 7;
// RootCert is the PEM-encoded public certificate.
string RootCert = 8;
// IntermediateCerts is a list of PEM-encoded intermediate certs to
// attach to any leaf certs signed by this CA.
repeated string IntermediateCerts = 9;
// SigningCert is the PEM-encoded signing certificate and SigningKey
// is the PEM-encoded private key for the signing certificate. These
// may actually be empty if the CA plugin in use manages these for us.
string SigningCert = 10;
string SigningKey = 11;
// Active is true if this is the current active CA. This must only
// be true for exactly one CA. For any method that modifies roots in the
// state store, tests should be written to verify that multiple roots
// cannot be active.
bool Active = 12;
// RotatedOutAt is the time at which this CA was removed from the state.
// This will only be set on roots that have been rotated out from being the
// active root.
google.protobuf.Timestamp RotatedOutAt = 13;
// PrivateKeyType is the type of the private key used to sign certificates. It
// may be "rsa" or "ec". This is provided as a convenience to avoid parsing
// the public key to from the certificate to infer the type.
string PrivateKeyType = 14;
// PrivateKeyBits is the length of the private key used to sign certificates.
// This is provided as a convenience to avoid parsing the public key from the
// certificate to infer the type.
int32 PrivateKeyBits = 15;
common.RaftIndex RaftIndex = 16;
}
message IssuedCert {
// SerialNumber is the unique serial number for this certificate.
// This is encoded in standard hex separated by :.
string SerialNumber = 1;
// CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private
// key for that cert, respectively. This should not be stored in the
// state store, but is present in the sign API response.
string CertPEM = 2;
string PrivateKeyPEM = 3;
// Service is the name of the service for which the cert was issued.
// ServiceURI is the cert URI value.
string Service = 4;
string ServiceURI = 5;
// Agent is the name of the node for which the cert was issued.
// AgentURI is the cert URI value.
string Agent = 6;
string AgentURI = 7;
// ValidAfter and ValidBefore are the validity periods for the
// certificate.
google.protobuf.Timestamp ValidAfter = 8;
google.protobuf.Timestamp ValidBefore = 9;
// EnterpriseMeta is the Consul Enterprise specific metadata
common.EnterpriseMeta EnterpriseMeta = 10;
common.RaftIndex RaftIndex = 11;
}

56
proto/translate.go Normal file
View File

@ -0,0 +1,56 @@
package proto
import (
"reflect"
"time"
"github.com/gogo/protobuf/types"
)
var (
tsType = reflect.TypeOf((*types.Timestamp)(nil))
timePtrType = reflect.TypeOf((*time.Time)(nil))
timeType = timePtrType.Elem()
mapStrInf = reflect.TypeOf((map[string]interface{})(nil))
)
// HookPBTimestampToTime is a mapstructure decode hook to translate a protobuf timestamp
// to a time.Time value
func HookPBTimestampToTime(from, to reflect.Type, data interface{}) (interface{}, error) {
if to == timeType && from == tsType {
ts := data.(*types.Timestamp)
return time.Unix(ts.Seconds, int64(ts.Nanos)), nil
}
return data, nil
}
// HookTimeToPBtimestamp is a mapstructure decode hook to translate a time.Time value to
// a protobuf Timestamp value.
func HookTimeToPBTimestamp(from, to reflect.Type, data interface{}) (interface{}, error) {
// Note that mapstructure doesn't do direct struct to struct conversion in this case. I
// still don't completely understand why converting the PB TS to time.Time does but
// I suspect it has something to do with the struct containing a concrete time.Time
// as opposed to a pointer to a time.Time. Regardless this path through mapstructure
// first will decode the concrete time.Time into a map[string]interface{} before
// eventually decoding that map[string]interface{} into the *types.Timestamp. One
// other note is that mapstructure ends up creating a new Value and sets it it to
// the time.Time value and thats what gets passed to us. That is why we end up
// seeing a *time.Time instead of a time.Time.
if from == timePtrType && to == mapStrInf {
ts := data.(*time.Time)
nanos := ts.UnixNano()
if nanos < 0 {
return map[string]interface{}{}, nil
}
seconds := nanos / 1000000000
nanos = nanos % 1000000000
return map[string]interface{}{
"Seconds": seconds,
"Nanos": int32(nanos),
}, nil
}
return data, nil
}

65
proto/translate_test.go Normal file
View File

@ -0,0 +1,65 @@
package proto
import (
"testing"
"time"
"github.com/gogo/protobuf/types"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
)
type pbTSWrapper struct {
Timestamp *types.Timestamp
}
type timeTSWrapper struct {
Timestamp time.Time
}
func TestHookPBTimestampToTime(t *testing.T) {
in := pbTSWrapper{
Timestamp: &types.Timestamp{
Seconds: 1000,
Nanos: 42,
},
}
expected := timeTSWrapper{
Timestamp: time.Unix(1000, 42),
}
var actual timeTSWrapper
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookPBTimestampToTime,
Result: &actual,
})
require.NoError(t, err)
require.NoError(t, decoder.Decode(in))
require.Equal(t, expected, actual)
}
func TestHookTimeToPBTimestamp(t *testing.T) {
in := timeTSWrapper{
Timestamp: time.Unix(999999, 123456),
}
expected := pbTSWrapper{
Timestamp: &types.Timestamp{
Seconds: 999999,
Nanos: 123456,
},
}
var actual pbTSWrapper
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: HookTimeToPBTimestamp,
Result: &actual,
})
require.NoError(t, err)
require.NoError(t, decoder.Decode(in))
require.Equal(t, expected, actual)
}