mirror of https://github.com/status-im/consul.git
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:
parent
91ec880e07
commit
e813445e57
|
@ -461,6 +461,10 @@ func New(options ...AgentOption) (*Agent, error) {
|
||||||
// loaded any auto-config sources yet.
|
// loaded any auto-config sources yet.
|
||||||
a.config = config
|
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 {
|
if flat.logger == nil {
|
||||||
logConf := &logging.Config{
|
logConf := &logging.Config{
|
||||||
LogLevel: config.LogLevel,
|
LogLevel: config.LogLevel,
|
||||||
|
@ -526,14 +530,34 @@ func New(options ...AgentOption) (*Agent, error) {
|
||||||
return nil, fmt.Errorf("Failed to setup node ID: %v", err)
|
return nil, fmt.Errorf("Failed to setup node ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
acOpts := []autoconf.Option{
|
// We used to do this in the Start method. However it doesn't need to go
|
||||||
autoconf.WithDirectRPC(a.connPool),
|
// there any longer. Originally it did because we passed the agent
|
||||||
autoconf.WithTLSConfigurator(a.tlsConfigurator),
|
// delegate to some of the cache registrations. Now we just
|
||||||
autoconf.WithBuilderOpts(flat.builderOpts),
|
// pass the agent itself so its safe to move here.
|
||||||
autoconf.WithLogger(a.logger),
|
a.registerCache()
|
||||||
autoconf.WithOverrides(flat.overrides...),
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -664,9 +688,6 @@ func (a *Agent) Start(ctx context.Context) error {
|
||||||
// regular and on-demand state synchronizations (anti-entropy).
|
// regular and on-demand state synchronizations (anti-entropy).
|
||||||
a.sync = ae.NewStateSyncer(a.State, c.AEInterval, a.shutdownCh, a.logger)
|
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
|
// create the config for the rpc server/client
|
||||||
consulCfg, err := a.consulConfig()
|
consulCfg, err := a.consulConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -715,10 +736,6 @@ func (a *Agent) Start(ctx context.Context) error {
|
||||||
a.State.Delegate = a.delegate
|
a.State.Delegate = a.delegate
|
||||||
a.State.TriggerSyncChanges = a.sync.SyncChanges.Trigger
|
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 {
|
if a.config.AutoEncryptTLS && !a.config.ServerMode {
|
||||||
reply, err := a.autoEncryptInitialCertificate(ctx)
|
reply, err := a.autoEncryptInitialCertificate(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -756,6 +773,9 @@ func (a *Agent) Start(ctx context.Context) error {
|
||||||
a.logger.Info("automatically upgraded to TLS")
|
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()
|
a.serviceManager.Start()
|
||||||
|
|
||||||
// Load checks/services/metadata.
|
// Load checks/services/metadata.
|
||||||
|
@ -868,6 +888,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)
|
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 {
|
func (a *Agent) listenAndServeGRPC() error {
|
||||||
if len(a.config.GRPCAddrs) < 1 {
|
if len(a.config.GRPCAddrs) < 1 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -1822,6 +1846,16 @@ func (a *Agent) ShutdownAgent() error {
|
||||||
// Stop the watches to avoid any notification/state change during shutdown
|
// Stop the watches to avoid any notification/state change during shutdown
|
||||||
a.stopAllWatches()
|
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)
|
// Stop the service manager (must happen before we take the stateLock to avoid deadlock)
|
||||||
if a.serviceManager != nil {
|
if a.serviceManager != nil {
|
||||||
a.serviceManager.Stop()
|
a.serviceManager.Stop()
|
||||||
|
|
|
@ -4651,7 +4651,8 @@ func TestAutoConfig_Integration(t *testing.T) {
|
||||||
// eventually this test should really live with integration tests
|
// eventually this test should really live with integration tests
|
||||||
// the goal here is to have one test server and another test client
|
// 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
|
// 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")
|
cfgDir := testutil.TempDir(t, "auto-config")
|
||||||
|
|
||||||
|
@ -4689,7 +4690,6 @@ func TestAutoConfig_Integration(t *testing.T) {
|
||||||
cert_file = "` + certFile + `"
|
cert_file = "` + certFile + `"
|
||||||
key_file = "` + keyFile + `"
|
key_file = "` + keyFile + `"
|
||||||
connect { enabled = true }
|
connect { enabled = true }
|
||||||
auto_encrypt { allow_tls = true }
|
|
||||||
auto_config {
|
auto_config {
|
||||||
authorization {
|
authorization {
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -4746,11 +4746,44 @@ func TestAutoConfig_Integration(t *testing.T) {
|
||||||
|
|
||||||
defer client.Shutdown()
|
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
|
// 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
|
// and then connect. Additionally we would have to have certificates or else the
|
||||||
// verify_incoming config on the server would not let it work.
|
// verify_incoming config on the server would not let it work.
|
||||||
testrpc.WaitForTestAgent(t, client.RPC, "dc1", testrpc.WithToken(TestDefaultMasterToken))
|
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
|
// spot check that we now have an ACL token
|
||||||
require.NotEmpty(t, client.tokens.AgentToken())
|
require.NotEmpty(t, client.tokens.AgentToken())
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,84 +13,38 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"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/lib"
|
||||||
"github.com/hashicorp/consul/logging"
|
"github.com/hashicorp/consul/logging"
|
||||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||||
"github.com/hashicorp/consul/tlsutil"
|
|
||||||
"github.com/hashicorp/go-discover"
|
"github.com/hashicorp/go-discover"
|
||||||
discoverk8s "github.com/hashicorp/go-discover/provider/k8s"
|
discoverk8s "github.com/hashicorp/go-discover/provider/k8s"
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/jsonpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// autoConfigFileName is the name of the file that the agent auto-config settings are
|
// autoConfigFileName is the name of the file that the agent auto-config settings are
|
||||||
// stored in within the data directory
|
// stored in within the data directory
|
||||||
autoConfigFileName = "auto-config.json"
|
autoConfigFileName = "auto-config.json"
|
||||||
|
|
||||||
|
dummyTrustDomain = "dummytrustdomain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DirectRPC is the interface that needs to be satisifed for AutoConfig to be able to perform
|
var (
|
||||||
// direct RPCs against individual servers. This should not use
|
pbMarshaler = &jsonpb.Marshaler{
|
||||||
type DirectRPC interface {
|
OrigName: false,
|
||||||
RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error
|
EnumsAsInts: false,
|
||||||
}
|
Indent: " ",
|
||||||
|
EmitDefaults: true,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// WithTLSConfigurator will cause the created AutoConfig type to use the provided configurator
|
pbUnmarshaler = &jsonpb.Unmarshaler{
|
||||||
func WithTLSConfigurator(tlsConfigurator *tlsutil.Configurator) Option {
|
AllowUnknownFields: false,
|
||||||
return func(opt *options) {
|
|
||||||
opt.tlsConfigurator = tlsConfigurator
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
// 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
|
// 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.
|
// 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
|
// 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.
|
// to help ease the review of this already large PR.
|
||||||
type AutoConfig struct {
|
type AutoConfig struct {
|
||||||
config *config.RuntimeConfig
|
builderOpts config.BuilderOpts
|
||||||
builderOpts config.BuilderOpts
|
logger hclog.Logger
|
||||||
logger hclog.Logger
|
directRPC DirectRPC
|
||||||
directRPC DirectRPC
|
waiter *lib.RetryWaiter
|
||||||
tlsConfigurator *tlsutil.Configurator
|
overrides []config.Source
|
||||||
autoConfigData string
|
certMonitor CertMonitor
|
||||||
waiter *lib.RetryWaiter
|
config *config.RuntimeConfig
|
||||||
overrides []config.Source
|
autoConfigData string
|
||||||
}
|
cancel context.CancelFunc
|
||||||
|
|
||||||
func flattenOptions(opts []Option) options {
|
|
||||||
var flat options
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&flat)
|
|
||||||
}
|
|
||||||
return flat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new AutoConfig object for providing automatic
|
// New creates a new AutoConfig object for providing automatic
|
||||||
// Consul configuration.
|
// Consul configuration.
|
||||||
func New(options ...Option) (*AutoConfig, error) {
|
func New(config *Config) (*AutoConfig, error) {
|
||||||
flat := flattenOptions(options)
|
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")
|
return nil, fmt.Errorf("must provide a direct RPC delegate")
|
||||||
}
|
}
|
||||||
|
|
||||||
if flat.tlsConfigurator == nil {
|
logger := config.Logger
|
||||||
return nil, fmt.Errorf("must provide a TLS configurator")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := flat.logger
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = hclog.NewNullLogger()
|
logger = hclog.NewNullLogger()
|
||||||
} else {
|
} else {
|
||||||
logger = logger.Named(logging.AutoConfig)
|
logger = logger.Named(logging.AutoConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
waiter := flat.waiter
|
waiter := config.Waiter
|
||||||
if waiter == nil {
|
if waiter == nil {
|
||||||
waiter = lib.NewRetryWaiter(1, 0, 10*time.Minute, lib.NewJitterRandomStagger(25))
|
waiter = lib.NewRetryWaiter(1, 0, 10*time.Minute, lib.NewJitterRandomStagger(25))
|
||||||
}
|
}
|
||||||
|
|
||||||
ac := &AutoConfig{
|
ac := &AutoConfig{
|
||||||
builderOpts: flat.builderOpts,
|
builderOpts: config.BuilderOpts,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
directRPC: flat.directRPC,
|
directRPC: config.DirectRPC,
|
||||||
tlsConfigurator: flat.tlsConfigurator,
|
waiter: waiter,
|
||||||
waiter: waiter,
|
overrides: config.Overrides,
|
||||||
overrides: flat.overrides,
|
certMonitor: config.CertMonitor,
|
||||||
}
|
}
|
||||||
|
|
||||||
return ac, nil
|
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
|
// ReadConfig will parse the current configuration and inject any
|
||||||
// auto-config sources if present into the correct place in the parsing chain.
|
// auto-config sources if present into the correct place in the parsing chain.
|
||||||
func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) {
|
func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) {
|
||||||
|
@ -219,8 +139,18 @@ func (ac *AutoConfig) restorePersistedAutoConfig() (bool, error) {
|
||||||
|
|
||||||
content, err := ioutil.ReadFile(path)
|
content, err := ioutil.ReadFile(path)
|
||||||
if err == nil {
|
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.logger.Info("restored persisted configuration", "path", path)
|
||||||
ac.autoConfigData = string(content)
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,12 +235,12 @@ func (ac *AutoConfig) introToken() (string, error) {
|
||||||
return token, nil
|
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
|
// 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
|
// of hosts. These might be hostnames and is expected that DNS resolution may
|
||||||
// be performed after this function runs. Additionally these may contain ports
|
// be performed after this function runs. Additionally these may contain ports
|
||||||
// so SplitHostPort could also be necessary.
|
// so SplitHostPort could also be necessary.
|
||||||
func (ac *AutoConfig) autoConfigHosts() ([]string, error) {
|
func (ac *AutoConfig) serverHosts() ([]string, error) {
|
||||||
servers := ac.config.AutoConfig.ServerAddresses
|
servers := ac.config.AutoConfig.ServerAddresses
|
||||||
|
|
||||||
providers := make(map[string]discover.Provider)
|
providers := make(map[string]discover.Provider)
|
||||||
|
@ -388,31 +318,20 @@ func (ac *AutoConfig) resolveHost(hostPort string) []net.TCPAddr {
|
||||||
return addrs
|
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
|
// This will persist the configuration to disk (unless in dev mode running without
|
||||||
// a data dir) and will reload the configuration.
|
// a data dir) and will reload the configuration.
|
||||||
func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse) error {
|
func (ac *AutoConfig) recordResponse(resp *pbautoconf.AutoConfigResponse) error {
|
||||||
// overwrite the auto encrypt DNS SANs with the ones specified in the auto_config stanza
|
serialized, err := pbMarshaler.MarshalToString(resp)
|
||||||
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))
|
|
||||||
if err != nil {
|
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 == "" {
|
if ac.config.DataDir == "" {
|
||||||
ac.logger.Debug("not persisting auto-config settings because there is no data directory")
|
ac.logger.Debug("not persisting auto-config settings because there is no data directory")
|
||||||
return nil
|
return nil
|
||||||
|
@ -420,7 +339,7 @@ func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse
|
||||||
|
|
||||||
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
|
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
|
||||||
|
|
||||||
err = ioutil.WriteFile(path, conf, 0660)
|
err = ioutil.WriteFile(path, []byte(serialized), 0660)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to write auto-config configurations: %w", err)
|
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
|
// 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
|
// successfully recorded the auto config settings (persisted to disk and stored internally
|
||||||
// on the AutoConfig object)
|
// 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()
|
token, err := ac.introToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
request := pbautoconf.AutoConfigRequest{
|
request := pbautoconf.AutoConfigRequest{
|
||||||
|
@ -446,50 +365,63 @@ func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context) (bool, er
|
||||||
Node: ac.config.NodeName,
|
Node: ac.config.NodeName,
|
||||||
Segment: ac.config.SegmentName,
|
Segment: ac.config.SegmentName,
|
||||||
JWT: token,
|
JWT: token,
|
||||||
|
CSR: csr,
|
||||||
}
|
}
|
||||||
|
|
||||||
var reply pbautoconf.AutoConfigResponse
|
var resp pbautoconf.AutoConfigResponse
|
||||||
|
|
||||||
servers, err := ac.autoConfigHosts()
|
servers, err := ac.serverHosts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range servers {
|
for _, s := range servers {
|
||||||
// try each IP to see if we can successfully make the request
|
// try each IP to see if we can successfully make the request
|
||||||
for _, addr := range ac.resolveHost(s) {
|
for _, addr := range ac.resolveHost(s) {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return false, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
ac.logger.Debug("making AutoConfig.InitialConfiguration RPC", "addr", addr.String())
|
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)
|
ac.logger.Error("AutoConfig.InitialConfiguration RPC failed", "addr", addr.String(), "error", err)
|
||||||
continue
|
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.
|
// getInitialConfiguration implements a loop to retry calls to getInitialConfigurationOnce.
|
||||||
// It uses the RetryWaiter on the AutoConfig object to control how often to attempt
|
// 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.
|
// the initial configuration process. It is also canceallable by cancelling the provided context.
|
||||||
func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) error {
|
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
|
// this resets the failures so that we will perform immediate request
|
||||||
wait := ac.waiter.Success()
|
wait := ac.waiter.Success()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-wait:
|
case <-wait:
|
||||||
done, err := ac.getInitialConfigurationOnce(ctx)
|
resp, err := ac.getInitialConfigurationOnce(ctx, csr, key)
|
||||||
if done {
|
if resp != nil {
|
||||||
return err
|
return ac.recordResponse(resp)
|
||||||
}
|
} else if err != nil {
|
||||||
if err != nil {
|
|
||||||
ac.logger.Error(err.Error())
|
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()
|
wait = ac.waiter.Failed()
|
||||||
case <-ctx.Done():
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -2,21 +2,23 @@ package autoconf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gogo/protobuf/types"
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/lib"
|
"github.com/hashicorp/consul/lib"
|
||||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||||
"github.com/hashicorp/consul/proto/pbconfig"
|
"github.com/hashicorp/consul/proto/pbconfig"
|
||||||
|
"github.com/hashicorp/consul/proto/pbconnect"
|
||||||
"github.com/hashicorp/consul/sdk/testutil"
|
"github.com/hashicorp/consul/sdk/testutil"
|
||||||
"github.com/hashicorp/consul/tlsutil"
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"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 {
|
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) {
|
switch ret := retValues.Get(0).(type) {
|
||||||
case error:
|
case error:
|
||||||
return ret
|
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) {
|
func TestNew(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
opts []Option
|
config Config
|
||||||
err string
|
err string
|
||||||
validate func(t *testing.T, ac *AutoConfig)
|
validate func(t *testing.T, ac *AutoConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := map[string]testCase{
|
cases := map[string]testCase{
|
||||||
"no-direct-rpc": {
|
"no-direct-rpc": {
|
||||||
opts: []Option{
|
|
||||||
WithTLSConfigurator(&tlsutil.Configurator{}),
|
|
||||||
},
|
|
||||||
err: "must provide a direct RPC delegate",
|
err: "must provide a direct RPC delegate",
|
||||||
},
|
},
|
||||||
"no-tls-configurator": {
|
|
||||||
opts: []Option{
|
|
||||||
WithDirectRPC(&mockDirectRPC{}),
|
|
||||||
},
|
|
||||||
err: "must provide a TLS configurator",
|
|
||||||
},
|
|
||||||
"ok": {
|
"ok": {
|
||||||
opts: []Option{
|
config: Config{
|
||||||
WithTLSConfigurator(&tlsutil.Configurator{}),
|
DirectRPC: &mockDirectRPC{},
|
||||||
WithDirectRPC(&mockDirectRPC{}),
|
|
||||||
},
|
},
|
||||||
validate: func(t *testing.T, ac *AutoConfig) {
|
validate: func(t *testing.T, ac *AutoConfig) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
@ -72,7 +102,7 @@ func TestNew(t *testing.T) {
|
||||||
|
|
||||||
for name, tcase := range cases {
|
for name, tcase := range cases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
ac, err := New(tcase.opts...)
|
ac, err := New(&tcase.config)
|
||||||
if tcase.err != "" {
|
if tcase.err != "" {
|
||||||
testutil.RequireErrorContains(t, err, tcase.err)
|
testutil.RequireErrorContains(t, err, tcase.err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -173,7 +203,10 @@ func TestInitialConfiguration_disabled(t *testing.T) {
|
||||||
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
||||||
|
|
||||||
directRPC := mockDirectRPC{}
|
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.NoError(t, err)
|
||||||
require.NotNil(t, ac)
|
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)
|
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.NoError(t, err)
|
||||||
require.NotNil(t, ac)
|
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
|
// persist an auto config response to the data dir where it is expected
|
||||||
persistedFile := filepath.Join(dataDir, autoConfigFileName)
|
persistedFile := filepath.Join(dataDir, autoConfigFileName)
|
||||||
response := &pbconfig.Config{
|
response := &pbautoconf.AutoConfigResponse{
|
||||||
PrimaryDatacenter: "primary",
|
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, err)
|
||||||
require.NoError(t, ioutil.WriteFile(persistedFile, data, 0600))
|
require.NoError(t, ioutil.WriteFile(persistedFile, []byte(data), 0600))
|
||||||
|
|
||||||
directRPC := mockDirectRPC{}
|
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.NoError(t, err)
|
||||||
require.NotNil(t, ac)
|
require.NotNil(t, ac)
|
||||||
|
|
||||||
cfg, err := ac.InitialConfiguration(context.Background())
|
cfg, err := ac.InitialConfiguration(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err, data)
|
||||||
require.NotNil(t, cfg)
|
require.NotNil(t, cfg)
|
||||||
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
||||||
|
|
||||||
// ensure no RPC was made
|
// ensure no RPC was made
|
||||||
directRPC.AssertExpectations(t)
|
directRPC.AssertExpectations(t)
|
||||||
|
certMon.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitialConfiguration_success(t *testing.T) {
|
func TestInitialConfiguration_success(t *testing.T) {
|
||||||
|
@ -275,7 +382,36 @@ func TestInitialConfiguration_success(t *testing.T) {
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
resp.Config = &pbconfig.Config{
|
resp.Config = &pbconfig.Config{
|
||||||
PrimaryDatacenter: "primary",
|
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{
|
expectedRequest := pbautoconf.AutoConfigRequest{
|
||||||
|
@ -293,7 +429,44 @@ func TestInitialConfiguration_success(t *testing.T) {
|
||||||
&expectedRequest,
|
&expectedRequest,
|
||||||
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
|
&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.NoError(t, err)
|
||||||
require.NotNil(t, ac)
|
require.NotNil(t, ac)
|
||||||
|
|
||||||
|
@ -307,6 +480,7 @@ func TestInitialConfiguration_success(t *testing.T) {
|
||||||
|
|
||||||
// ensure no RPC was made
|
// ensure no RPC was made
|
||||||
directRPC.AssertExpectations(t)
|
directRPC.AssertExpectations(t)
|
||||||
|
certMon.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitialConfiguration_retries(t *testing.T) {
|
func TestInitialConfiguration_retries(t *testing.T) {
|
||||||
|
@ -381,7 +555,11 @@ func TestInitialConfiguration_retries(t *testing.T) {
|
||||||
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
|
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
|
||||||
|
|
||||||
waiter := lib.NewRetryWaiter(2, 0, 1*time.Millisecond, nil)
|
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.NoError(t, err)
|
||||||
require.NotNil(t, ac)
|
require.NotNil(t, ac)
|
||||||
|
|
||||||
|
@ -396,3 +574,162 @@ func TestInitialConfiguration_retries(t *testing.T) {
|
||||||
// ensure no RPC was made
|
// ensure no RPC was made
|
||||||
directRPC.AssertExpectations(t)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,7 +1,14 @@
|
||||||
package autoconf
|
package autoconf
|
||||||
|
|
||||||
import (
|
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/pbconfig"
|
||||||
|
"github.com/hashicorp/consul/proto/pbconnect"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
// translateAgentConfig is meant to take in a proto/pbconfig.Config type
|
// 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
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -230,6 +230,7 @@ func (m *CertMonitor) Start(ctx context.Context) (<-chan struct{}, error) {
|
||||||
exit := make(chan struct{})
|
exit := make(chan struct{})
|
||||||
go m.run(ctx, exit)
|
go m.run(ctx, exit)
|
||||||
|
|
||||||
|
m.logger.Info("certificate monitor started")
|
||||||
return exit, nil
|
return exit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1297,6 +1297,10 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
|
||||||
return err
|
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 {
|
if err := b.validateAutoConfig(rt); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1692,10 +1692,6 @@ func (c *RuntimeConfig) ClientAddress() (unixAddr, httpAddr, httpsAddr string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RuntimeConfig) ConnectCAConfiguration() (*structs.CAConfiguration, error) {
|
func (c *RuntimeConfig) ConnectCAConfiguration() (*structs.CAConfiguration, error) {
|
||||||
if !c.ConnectEnabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ca := &structs.CAConfiguration{
|
ca := &structs.CAConfiguration{
|
||||||
Provider: "consul",
|
Provider: "consul",
|
||||||
Config: map[string]interface{}{
|
Config: map[string]interface{}{
|
||||||
|
|
|
@ -3804,6 +3804,35 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
|
||||||
|
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
// Auto Config related tests
|
// 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",
|
desc: "auto config not allowed for servers",
|
||||||
args: []string{
|
args: []string{
|
||||||
|
@ -7480,12 +7509,6 @@ func TestConnectCAConfiguration(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := map[string]testCase{
|
cases := map[string]testCase{
|
||||||
"connect-disabled": {
|
|
||||||
config: RuntimeConfig{
|
|
||||||
ConnectEnabled: false,
|
|
||||||
},
|
|
||||||
expected: nil,
|
|
||||||
},
|
|
||||||
"defaults": {
|
"defaults": {
|
||||||
config: RuntimeConfig{
|
config: RuntimeConfig{
|
||||||
ConnectEnabled: true,
|
ConnectEnabled: true,
|
||||||
|
|
|
@ -2,22 +2,31 @@ package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"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/consul/authmethod/ssoauth"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/lib/template"
|
"github.com/hashicorp/consul/lib/template"
|
||||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
"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"
|
"github.com/hashicorp/consul/tlsutil"
|
||||||
bexpr "github.com/hashicorp/go-bexpr"
|
bexpr "github.com/hashicorp/go-bexpr"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AutoConfigOptions struct {
|
type AutoConfigOptions struct {
|
||||||
NodeName string
|
NodeName string
|
||||||
SegmentName string
|
SegmentName string
|
||||||
|
|
||||||
|
CSR *x509.CertificateRequest
|
||||||
|
SpiffeID *connect.SpiffeIDAgent
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutoConfigAuthorizer interface {
|
type AutoConfigAuthorizer interface {
|
||||||
|
@ -73,16 +82,35 @@ func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AutoConfigOptions{
|
opts := AutoConfigOptions{
|
||||||
NodeName: req.Node,
|
NodeName: req.Node,
|
||||||
SegmentName: req.Segment,
|
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 {
|
type AutoConfigBackend interface {
|
||||||
CreateACLToken(template *structs.ACLToken) (*structs.ACLToken, error)
|
CreateACLToken(template *structs.ACLToken) (*structs.ACLToken, error)
|
||||||
DatacenterJoinAddresses(segment string) ([]string, error)
|
DatacenterJoinAddresses(segment string) ([]string, error)
|
||||||
ForwardRPC(method string, info structs.RPCInfo, args, reply interface{}) (bool, 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
|
// 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
|
// 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
|
// 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.
|
// to configure auto_encrypt or potentially in the future to generate the certificates inline.
|
||||||
func (ac *AutoConfig) updateTLSCertificatesInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
func (ac *AutoConfig) updateTLSCertificatesInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||||
conf.AutoEncrypt = &config.AutoEncrypt{TLS: ac.config.AutoEncryptAllowTLS}
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateACLtokensInConfig will configure all of the agents ACL settings and will populate
|
// updateACLtokensInConfig will configure all of the agents ACL settings and will populate
|
||||||
// the configuration with an agent token usable for all default agent operations.
|
// the configuration with an agent token usable for all default agent operations.
|
||||||
func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||||
acl := &config.ACL{
|
acl := &pbconfig.ACL{
|
||||||
Enabled: ac.config.ACLsEnabled,
|
Enabled: ac.config.ACLsEnabled,
|
||||||
PolicyTTL: ac.config.ACLPolicyTTL.String(),
|
PolicyTTL: ac.config.ACLPolicyTTL.String(),
|
||||||
RoleTTL: ac.config.ACLRoleTTL.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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateJoinAddressesInConfig determines the correct gossip endpoints that clients should
|
// 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.
|
// 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)
|
joinAddrs, err := ac.backend.DatacenterJoinAddresses(opts.SegmentName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Gossip == nil {
|
if resp.Config.Gossip == nil {
|
||||||
conf.Gossip = &config.Gossip{}
|
resp.Config.Gossip = &pbconfig.Gossip{}
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.Gossip.RetryJoinLAN = joinAddrs
|
resp.Config.Gossip.RetryJoinLAN = joinAddrs
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateGossipEncryptionInConfig will populate the gossip encryption configuration settings
|
// 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
|
// Add gossip encryption settings if there is any key loaded
|
||||||
memberlistConfig := ac.config.SerfLANConfig.MemberlistConfig
|
memberlistConfig := ac.config.SerfLANConfig.MemberlistConfig
|
||||||
if lanKeyring := memberlistConfig.Keyring; lanKeyring != nil {
|
if lanKeyring := memberlistConfig.Keyring; lanKeyring != nil {
|
||||||
if conf.Gossip == nil {
|
if resp.Config.Gossip == nil {
|
||||||
conf.Gossip = &config.Gossip{}
|
resp.Config.Gossip = &pbconfig.Gossip{}
|
||||||
}
|
}
|
||||||
if conf.Gossip.Encryption == nil {
|
if resp.Config.Gossip.Encryption == nil {
|
||||||
conf.Gossip.Encryption = &config.GossipEncryption{}
|
resp.Config.Gossip.Encryption = &pbconfig.GossipEncryption{}
|
||||||
}
|
}
|
||||||
|
|
||||||
pk := lanKeyring.GetPrimaryKey()
|
pk := lanKeyring.GetPrimaryKey()
|
||||||
if len(pk) > 0 {
|
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
|
resp.Config.Gossip.Encryption.VerifyIncoming = memberlistConfig.GossipVerifyIncoming
|
||||||
conf.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
|
resp.Config.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -202,44 +266,44 @@ func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *
|
||||||
|
|
||||||
// updateTLSSettingsInConfig will populate the TLS configuration settings but will not
|
// updateTLSSettingsInConfig will populate the TLS configuration settings but will not
|
||||||
// populate leaf or ca certficiates.
|
// 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 {
|
if ac.tlsConfigurator == nil {
|
||||||
// TLS is not enabled?
|
// TLS is not enabled?
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// add in TLS configuration
|
// add in TLS configuration
|
||||||
if conf.TLS == nil {
|
if resp.Config.TLS == nil {
|
||||||
conf.TLS = &config.TLS{}
|
resp.Config.TLS = &pbconfig.TLS{}
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.TLS.VerifyServerHostname = ac.tlsConfigurator.VerifyServerHostname()
|
resp.Config.TLS.VerifyServerHostname = ac.tlsConfigurator.VerifyServerHostname()
|
||||||
base := ac.tlsConfigurator.Base()
|
base := ac.tlsConfigurator.Base()
|
||||||
conf.TLS.VerifyOutgoing = base.VerifyOutgoing
|
resp.Config.TLS.VerifyOutgoing = base.VerifyOutgoing
|
||||||
conf.TLS.MinVersion = base.TLSMinVersion
|
resp.Config.TLS.MinVersion = base.TLSMinVersion
|
||||||
conf.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
|
resp.Config.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
conf.TLS.CipherSuites, err = tlsutil.CipherString(base.CipherSuites)
|
resp.Config.TLS.CipherSuites, err = tlsutil.CipherString(base.CipherSuites)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// baseConfig will populate the configuration with some base settings such as the
|
// baseConfig will populate the configuration with some base settings such as the
|
||||||
// datacenter names, node name etc.
|
// 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 == "" {
|
if opts.NodeName == "" {
|
||||||
return fmt.Errorf("Cannot generate auto config response without a node name")
|
return fmt.Errorf("Cannot generate auto config response without a node name")
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.Datacenter = ac.config.Datacenter
|
resp.Config.Datacenter = ac.config.Datacenter
|
||||||
conf.PrimaryDatacenter = ac.config.PrimaryDatacenter
|
resp.Config.PrimaryDatacenter = ac.config.PrimaryDatacenter
|
||||||
conf.NodeName = opts.NodeName
|
resp.Config.NodeName = opts.NodeName
|
||||||
conf.SegmentName = opts.SegmentName
|
resp.Config.SegmentName = opts.SegmentName
|
||||||
|
|
||||||
return nil
|
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 (
|
var (
|
||||||
// variable holding the list of config updating functions to execute when generating
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := &config.Config{}
|
resp.Config = &pbconfig.Config{}
|
||||||
|
|
||||||
// update all the configurations
|
// update all the configurations
|
||||||
for _, configFn := range autoConfigUpdaters {
|
for _, configFn := range autoConfigUpdaters {
|
||||||
if err := configFn(ac, opts, conf); err != nil {
|
if err := configFn(ac, opts, resp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.Config = conf
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package consul
|
package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -11,6 +12,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
||||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
"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)
|
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 {
|
func testJWTStandardClaims() jwt.Claims {
|
||||||
now := time.Now()
|
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
|
// * Each of the individual config generation functions. These can be unit tested separately and should NOT
|
||||||
// require running test servers
|
// require running test servers
|
||||||
func TestAutoConfigInitialConfiguration(t *testing.T) {
|
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)
|
gossipKey := make([]byte, 32)
|
||||||
// this is not cryptographic randomness and is not secure but for the sake of this test its all we need.
|
// 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)
|
n, err := rand.Read(gossipKey)
|
||||||
|
@ -105,85 +112,21 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
|
||||||
_, altpriv, err := oidcauthtest.GenerateKey()
|
_, altpriv, err := oidcauthtest.GenerateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
cases := map[string]testCase{
|
// this CSR is what gets sent in the request
|
||||||
"wrong-datacenter": {
|
csrID := connect.SpiffeIDAgent{
|
||||||
request: pbautoconf.AutoConfigRequest{
|
Host: "dummy.trustdomain",
|
||||||
Datacenter: "no-such-dc",
|
Agent: "test-node",
|
||||||
},
|
Datacenter: "dc1",
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
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) {
|
_, s, _ := testACLServerWithConfig(t, func(c *Config) {
|
||||||
c.Domain = "consul"
|
c.Domain = "consul"
|
||||||
|
@ -248,6 +191,126 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
|
||||||
|
|
||||||
waitForLeaderEstablishment(t, s)
|
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 {
|
for testName, tcase := range cases {
|
||||||
t.Run(testName, func(t *testing.T) {
|
t.Run(testName, func(t *testing.T) {
|
||||||
var reply pbautoconf.AutoConfigResponse
|
var reply pbautoconf.AutoConfigResponse
|
||||||
|
@ -269,7 +332,7 @@ func TestAutoConfig_baseConfig(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
serverConfig Config
|
serverConfig Config
|
||||||
opts AutoConfigOptions
|
opts AutoConfigOptions
|
||||||
expected pbconfig.Config
|
expected pbautoconf.AutoConfigResponse
|
||||||
err string
|
err string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,11 +346,13 @@ func TestAutoConfig_baseConfig(t *testing.T) {
|
||||||
NodeName: "lBdc0lsH",
|
NodeName: "lBdc0lsH",
|
||||||
SegmentName: "HZiwlWpi",
|
SegmentName: "HZiwlWpi",
|
||||||
},
|
},
|
||||||
expected: pbconfig.Config{
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
Datacenter: "oSWzfhnU",
|
Config: &pbconfig.Config{
|
||||||
PrimaryDatacenter: "53XO9mx4",
|
Datacenter: "oSWzfhnU",
|
||||||
NodeName: "lBdc0lsH",
|
PrimaryDatacenter: "53XO9mx4",
|
||||||
SegmentName: "HZiwlWpi",
|
NodeName: "lBdc0lsH",
|
||||||
|
SegmentName: "HZiwlWpi",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"no-node-name": {
|
"no-node-name": {
|
||||||
|
@ -305,7 +370,7 @@ func TestAutoConfig_baseConfig(t *testing.T) {
|
||||||
config: &tcase.serverConfig,
|
config: &tcase.serverConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
var actual pbconfig.Config
|
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||||
err := ac.baseConfig(tcase.opts, &actual)
|
err := ac.baseConfig(tcase.opts, &actual)
|
||||||
if tcase.err == "" {
|
if tcase.err == "" {
|
||||||
require.NoError(t, 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) {
|
func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
||||||
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
|
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -328,16 +400,9 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
||||||
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
|
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
|
||||||
require.NoError(t, err)
|
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 {
|
type testCase struct {
|
||||||
tlsConfig tlsutil.Config
|
tlsConfig tlsutil.Config
|
||||||
expected pbconfig.Config
|
expected pbautoconf.AutoConfigResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := map[string]testCase{
|
cases := map[string]testCase{
|
||||||
|
@ -350,13 +415,15 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
||||||
CAFile: cafile,
|
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"),
|
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{
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
TLS: &pbconfig.TLS{
|
Config: &pbconfig.Config{
|
||||||
VerifyOutgoing: true,
|
TLS: &pbconfig.TLS{
|
||||||
VerifyServerHostname: true,
|
VerifyOutgoing: true,
|
||||||
MinVersion: "tls12",
|
VerifyServerHostname: true,
|
||||||
PreferServerCipherSuites: true,
|
MinVersion: "tls12",
|
||||||
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",
|
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,
|
CAFile: cafile,
|
||||||
CipherSuites: parseCiphers(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
|
CipherSuites: parseCiphers(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
|
||||||
},
|
},
|
||||||
expected: pbconfig.Config{
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
TLS: &pbconfig.TLS{
|
Config: &pbconfig.Config{
|
||||||
VerifyOutgoing: true,
|
TLS: &pbconfig.TLS{
|
||||||
VerifyServerHostname: false,
|
VerifyOutgoing: true,
|
||||||
MinVersion: "tls10",
|
VerifyServerHostname: false,
|
||||||
PreferServerCipherSuites: false,
|
MinVersion: "tls10",
|
||||||
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
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,
|
tlsConfigurator: configurator,
|
||||||
}
|
}
|
||||||
|
|
||||||
var actual pbconfig.Config
|
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||||
err = ac.updateTLSSettingsInConfig(AutoConfigOptions{}, &actual)
|
err = ac.updateTLSSettingsInConfig(AutoConfigOptions{}, &actual)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tcase.expected, actual)
|
require.Equal(t, tcase.expected, actual)
|
||||||
|
@ -402,7 +471,7 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
||||||
func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
conf memberlist.Config
|
conf memberlist.Config
|
||||||
expected pbconfig.Config
|
expected pbautoconf.AutoConfigResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
gossipKey := make([]byte, 32)
|
gossipKey := make([]byte, 32)
|
||||||
|
@ -422,12 +491,14 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
||||||
GossipVerifyIncoming: true,
|
GossipVerifyIncoming: true,
|
||||||
GossipVerifyOutgoing: true,
|
GossipVerifyOutgoing: true,
|
||||||
},
|
},
|
||||||
expected: pbconfig.Config{
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
Gossip: &pbconfig.Gossip{
|
Config: &pbconfig.Config{
|
||||||
Encryption: &pbconfig.GossipEncryption{
|
Gossip: &pbconfig.Gossip{
|
||||||
Key: gossipKeyEncoded,
|
Encryption: &pbconfig.GossipEncryption{
|
||||||
VerifyIncoming: true,
|
Key: gossipKeyEncoded,
|
||||||
VerifyOutgoing: true,
|
VerifyIncoming: true,
|
||||||
|
VerifyOutgoing: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -438,12 +509,14 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
||||||
GossipVerifyIncoming: false,
|
GossipVerifyIncoming: false,
|
||||||
GossipVerifyOutgoing: false,
|
GossipVerifyOutgoing: false,
|
||||||
},
|
},
|
||||||
expected: pbconfig.Config{
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
Gossip: &pbconfig.Gossip{
|
Config: &pbconfig.Config{
|
||||||
Encryption: &pbconfig.GossipEncryption{
|
Gossip: &pbconfig.Gossip{
|
||||||
Key: gossipKeyEncoded,
|
Encryption: &pbconfig.GossipEncryption{
|
||||||
VerifyIncoming: false,
|
Key: gossipKeyEncoded,
|
||||||
VerifyOutgoing: false,
|
VerifyIncoming: false,
|
||||||
|
VerifyOutgoing: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -451,6 +524,9 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
||||||
"encryption-disabled": {
|
"encryption-disabled": {
|
||||||
// zero values all around - if no keyring is configured then the gossip
|
// zero values all around - if no keyring is configured then the gossip
|
||||||
// encryption settings should not be set.
|
// encryption settings should not be set.
|
||||||
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
|
Config: &pbconfig.Config{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,7 +539,7 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
var actual pbconfig.Config
|
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||||
err := ac.updateGossipEncryptionInConfig(AutoConfigOptions{}, &actual)
|
err := ac.updateGossipEncryptionInConfig(AutoConfigOptions{}, &actual)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tcase.expected, actual)
|
require.Equal(t, tcase.expected, actual)
|
||||||
|
@ -472,40 +548,149 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoConfig_updateTLSCertificatesInConfig(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 {
|
type testCase struct {
|
||||||
serverConfig Config
|
serverConfig Config
|
||||||
expected pbconfig.Config
|
tlsConfig tlsutil.Config
|
||||||
|
|
||||||
|
opts AutoConfigOptions
|
||||||
|
expected pbautoconf.AutoConfigResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := map[string]testCase{
|
cases := map[string]testCase{
|
||||||
"auto_encrypt-enabled": {
|
"no-csr": {
|
||||||
serverConfig: Config{
|
serverConfig: Config{
|
||||||
ConnectEnabled: true,
|
ConnectEnabled: true,
|
||||||
AutoEncryptAllowTLS: true,
|
|
||||||
},
|
},
|
||||||
expected: pbconfig.Config{
|
tlsConfig: tlsutil.Config{
|
||||||
AutoEncrypt: &pbconfig.AutoEncrypt{TLS: true},
|
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{
|
serverConfig: Config{
|
||||||
ConnectEnabled: true,
|
ConnectEnabled: true,
|
||||||
AutoEncryptAllowTLS: false,
|
|
||||||
},
|
},
|
||||||
expected: pbconfig.Config{
|
tlsConfig: tlsutil.Config{
|
||||||
AutoEncrypt: &pbconfig.AutoEncrypt{TLS: false},
|
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 {
|
for name, tcase := range cases {
|
||||||
t.Run(name, func(t *testing.T) {
|
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{
|
ac := AutoConfig{
|
||||||
config: &tcase.serverConfig,
|
config: &tcase.serverConfig,
|
||||||
|
tlsConfigurator: tlsConfigurator,
|
||||||
|
backend: backend,
|
||||||
}
|
}
|
||||||
|
|
||||||
var actual pbconfig.Config
|
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||||
err := ac.updateTLSCertificatesInConfig(AutoConfigOptions{}, &actual)
|
err = ac.updateTLSCertificatesInConfig(tcase.opts, &actual)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tcase.expected, actual)
|
require.Equal(t, tcase.expected, actual)
|
||||||
})
|
})
|
||||||
|
@ -515,7 +700,7 @@ func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
|
||||||
func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
config Config
|
config Config
|
||||||
expected pbconfig.Config
|
expected pbautoconf.AutoConfigResponse
|
||||||
expectACLToken bool
|
expectACLToken bool
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
@ -542,18 +727,20 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
||||||
ACLEnableKeyListPolicy: true,
|
ACLEnableKeyListPolicy: true,
|
||||||
},
|
},
|
||||||
expectACLToken: true,
|
expectACLToken: true,
|
||||||
expected: pbconfig.Config{
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
ACL: &pbconfig.ACL{
|
Config: &pbconfig.Config{
|
||||||
Enabled: true,
|
ACL: &pbconfig.ACL{
|
||||||
PolicyTTL: "7s",
|
Enabled: true,
|
||||||
RoleTTL: "10s",
|
PolicyTTL: "7s",
|
||||||
TokenTTL: "12s",
|
RoleTTL: "10s",
|
||||||
DisabledTTL: "31s",
|
TokenTTL: "12s",
|
||||||
DownPolicy: "deny",
|
DisabledTTL: "31s",
|
||||||
DefaultPolicy: "allow",
|
DownPolicy: "deny",
|
||||||
EnableKeyListPolicy: true,
|
DefaultPolicy: "allow",
|
||||||
Tokens: &pbconfig.ACLTokens{
|
EnableKeyListPolicy: true,
|
||||||
Agent: tokenSecret,
|
Tokens: &pbconfig.ACLTokens{
|
||||||
|
Agent: tokenSecret,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -572,16 +759,18 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
||||||
ACLEnableKeyListPolicy: true,
|
ACLEnableKeyListPolicy: true,
|
||||||
},
|
},
|
||||||
expectACLToken: false,
|
expectACLToken: false,
|
||||||
expected: pbconfig.Config{
|
expected: pbautoconf.AutoConfigResponse{
|
||||||
ACL: &pbconfig.ACL{
|
Config: &pbconfig.Config{
|
||||||
Enabled: false,
|
ACL: &pbconfig.ACL{
|
||||||
PolicyTTL: "7s",
|
Enabled: false,
|
||||||
RoleTTL: "10s",
|
PolicyTTL: "7s",
|
||||||
TokenTTL: "12s",
|
RoleTTL: "10s",
|
||||||
DisabledTTL: "31s",
|
TokenTTL: "12s",
|
||||||
DownPolicy: "deny",
|
DisabledTTL: "31s",
|
||||||
DefaultPolicy: "allow",
|
DownPolicy: "deny",
|
||||||
EnableKeyListPolicy: true,
|
DefaultPolicy: "allow",
|
||||||
|
EnableKeyListPolicy: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -630,7 +819,7 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
||||||
|
|
||||||
ac := AutoConfig{config: &tcase.config, backend: backend}
|
ac := AutoConfig{config: &tcase.config, backend: backend}
|
||||||
|
|
||||||
var actual pbconfig.Config
|
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||||
err := ac.updateACLsInConfig(AutoConfigOptions{NodeName: "something"}, &actual)
|
err := ac.updateACLsInConfig(AutoConfigOptions{NodeName: "something"}, &actual)
|
||||||
if tcase.err != nil {
|
if tcase.err != nil {
|
||||||
testutil.RequireErrorContains(t, err, tcase.err.Error())
|
testutil.RequireErrorContains(t, err, tcase.err.Error())
|
||||||
|
@ -651,12 +840,12 @@ func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
|
||||||
|
|
||||||
ac := AutoConfig{backend: backend}
|
ac := AutoConfig{backend: backend}
|
||||||
|
|
||||||
var actual pbconfig.Config
|
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||||
err := ac.updateJoinAddressesInConfig(AutoConfigOptions{}, &actual)
|
err := ac.updateJoinAddressesInConfig(AutoConfigOptions{}, &actual)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NotNil(t, actual.Gossip)
|
require.NotNil(t, actual.Config.Gossip)
|
||||||
require.ElementsMatch(t, addrs, actual.Gossip.RetryJoinLAN)
|
require.ElementsMatch(t, addrs, actual.Config.Gossip.RetryJoinLAN)
|
||||||
|
|
||||||
backend.AssertExpectations(t)
|
backend.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package consul
|
package consul
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -523,8 +524,8 @@ func NewServerWithOptions(config *Config, options ...ConsulOption) (*Server, err
|
||||||
return nil, fmt.Errorf("Failed to start Raft: %v", err)
|
return nil, fmt.Errorf("Failed to start Raft: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.config.ConnectEnabled && s.config.AutoEncryptAllowTLS {
|
if s.config.ConnectEnabled && (s.config.AutoEncryptAllowTLS || s.config.AutoConfigAuthzEnabled) {
|
||||||
go s.trackAutoEncryptCARoots()
|
go s.connectCARootsMonitor(&lib.StopChannelContext{StopCh: s.shutdownCh})
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.gatewayLocator != nil {
|
if s.gatewayLocator != nil {
|
||||||
|
@ -632,18 +633,11 @@ func NewServerWithOptions(config *Config, options ...ConsulOption) (*Server, err
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) trackAutoEncryptCARoots() {
|
func (s *Server) connectCARootsMonitor(ctx context.Context) {
|
||||||
for {
|
for {
|
||||||
select {
|
|
||||||
case <-s.shutdownCh:
|
|
||||||
s.logger.Debug("shutting down trackAutoEncryptCARoots because shutdown")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
ws := memdb.NewWatchSet()
|
ws := memdb.NewWatchSet()
|
||||||
state := s.fsm.State()
|
state := s.fsm.State()
|
||||||
ws.Add(state.AbandonCh())
|
ws.Add(state.AbandonCh())
|
||||||
ws.Add(s.shutdownCh)
|
|
||||||
_, cas, err := state.CARoots(ws)
|
_, cas, err := state.CARoots(ws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to watch AutoEncrypt CARoot", "error", err)
|
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 {
|
if err := s.tlsConfigurator.UpdateAutoEncryptCA(caPems); err != nil {
|
||||||
s.logger.Error("Failed to update AutoEncrypt CAPems", "error", err)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
fmt "fmt"
|
fmt "fmt"
|
||||||
proto "github.com/golang/protobuf/proto"
|
proto "github.com/golang/protobuf/proto"
|
||||||
pbconfig "github.com/hashicorp/consul/proto/pbconfig"
|
pbconfig "github.com/hashicorp/consul/proto/pbconfig"
|
||||||
|
pbconnect "github.com/hashicorp/consul/proto/pbconnect"
|
||||||
io "io"
|
io "io"
|
||||||
math "math"
|
math "math"
|
||||||
)
|
)
|
||||||
|
@ -39,7 +40,10 @@ type AutoConfigRequest struct {
|
||||||
JWT string `protobuf:"bytes,5,opt,name=JWT,proto3" json:"JWT,omitempty"`
|
JWT string `protobuf:"bytes,5,opt,name=JWT,proto3" json:"JWT,omitempty"`
|
||||||
// ConsulToken is a Consul ACL token that the agent requesting the
|
// ConsulToken is a Consul ACL token that the agent requesting the
|
||||||
// configuration already has.
|
// 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_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
XXX_unrecognized []byte `json:"-"`
|
||||||
XXX_sizecache int32 `json:"-"`
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
@ -113,12 +117,27 @@ func (m *AutoConfigRequest) GetConsulToken() string {
|
||||||
return ""
|
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
|
// AutoConfigResponse is the data structure sent in response to a AutoConfig.InitialConfiguration request
|
||||||
type AutoConfigResponse struct {
|
type AutoConfigResponse struct {
|
||||||
Config *pbconfig.Config `protobuf:"bytes,1,opt,name=Config,proto3" json:"Config,omitempty"`
|
// Config is the partial Consul configuration to inject into the agents own configuration
|
||||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
Config *pbconfig.Config `protobuf:"bytes,1,opt,name=Config,proto3" json:"Config,omitempty"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
// CARoots is the current list of Connect CA Roots
|
||||||
XXX_sizecache int32 `json:"-"`
|
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{} }
|
func (m *AutoConfigResponse) Reset() { *m = AutoConfigResponse{} }
|
||||||
|
@ -161,31 +180,60 @@ func (m *AutoConfigResponse) GetConfig() *pbconfig.Config {
|
||||||
return nil
|
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() {
|
func init() {
|
||||||
proto.RegisterType((*AutoConfigRequest)(nil), "autoconf.AutoConfigRequest")
|
proto.RegisterType((*AutoConfigRequest)(nil), "autoconf.AutoConfigRequest")
|
||||||
proto.RegisterType((*AutoConfigResponse)(nil), "autoconf.AutoConfigResponse")
|
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{
|
var fileDescriptor_ccc5af992e5daf69 = []byte{
|
||||||
// 256 bytes of a gzipped FileDescriptorProto
|
// 347 bytes of a gzipped FileDescriptorProto
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x2a, 0x28, 0xca, 0x2f,
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x92, 0xc1, 0x4a, 0xc3, 0x30,
|
||||||
0xc9, 0xd7, 0x2f, 0x48, 0x4a, 0x2c, 0x2d, 0xc9, 0x4f, 0xce, 0xcf, 0x4b, 0xd3, 0x07, 0x31, 0xe2,
|
0x18, 0xc7, 0x8d, 0x9b, 0x9b, 0x4b, 0x41, 0x66, 0x76, 0x09, 0x13, 0xcb, 0xd8, 0x41, 0x86, 0x48,
|
||||||
0x41, 0xac, 0xcc, 0x74, 0x3d, 0xb0, 0xa4, 0x10, 0x07, 0x4c, 0x4e, 0x4a, 0x1a, 0xa6, 0x1a, 0x22,
|
0x2b, 0x13, 0xaf, 0xc2, 0x8c, 0x1e, 0xf4, 0xe0, 0x21, 0x1b, 0x08, 0x5e, 0xa4, 0xeb, 0xb2, 0xad,
|
||||||
0xaf, 0x8f, 0xac, 0x4c, 0x69, 0x2a, 0x23, 0x97, 0xa0, 0x63, 0x69, 0x49, 0xbe, 0x33, 0x58, 0x30,
|
0xb8, 0x25, 0xb5, 0xf9, 0x02, 0x3e, 0x8a, 0x6f, 0xe0, 0xab, 0x78, 0xd4, 0x37, 0x90, 0xf9, 0x22,
|
||||||
0x28, 0xb5, 0xb0, 0x34, 0xb5, 0xb8, 0x44, 0x48, 0x8e, 0x8b, 0xcb, 0x25, 0xb1, 0x24, 0x31, 0x39,
|
0xd2, 0xb4, 0x95, 0x20, 0x9e, 0xfa, 0xef, 0xff, 0xf7, 0xfb, 0xe0, 0x6b, 0x13, 0xdc, 0x4f, 0x33,
|
||||||
0x35, 0xaf, 0x24, 0xb5, 0x48, 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x08, 0x49, 0x44, 0x48, 0x88,
|
0x05, 0x2a, 0x4c, 0xa7, 0x91, 0x01, 0x15, 0x2b, 0x39, 0x0f, 0xf3, 0xf0, 0x98, 0xa7, 0x64, 0x11,
|
||||||
0x8b, 0xc5, 0x2f, 0x3f, 0x25, 0x55, 0x82, 0x09, 0x2c, 0x03, 0x66, 0x0b, 0x49, 0x70, 0xb1, 0x07,
|
0x58, 0x48, 0x76, 0x2b, 0xd6, 0x3d, 0xa8, 0xec, 0x82, 0x87, 0xae, 0xd6, 0x3d, 0x74, 0xa0, 0x14,
|
||||||
0xa7, 0xa6, 0xe7, 0xa6, 0xe6, 0x95, 0x48, 0xb0, 0x80, 0x85, 0x61, 0x5c, 0x21, 0x01, 0x2e, 0x66,
|
0x31, 0x84, 0xe5, 0xb3, 0xc0, 0xfd, 0x37, 0x84, 0xf7, 0x47, 0x06, 0x14, 0xb3, 0x33, 0x5c, 0x3c,
|
||||||
0xaf, 0xf0, 0x10, 0x09, 0x56, 0xb0, 0x28, 0x88, 0x29, 0xa4, 0xc0, 0xc5, 0xed, 0x9c, 0x9f, 0x57,
|
0x1b, 0xa1, 0x81, 0xf8, 0x18, 0x5f, 0x45, 0x10, 0xc5, 0x42, 0x82, 0xc8, 0x28, 0xea, 0xa1, 0x41,
|
||||||
0x5c, 0x9a, 0x13, 0x92, 0x9f, 0x9d, 0x9a, 0x27, 0xc1, 0x06, 0x96, 0x41, 0x16, 0x52, 0xb2, 0xe1,
|
0x8b, 0x3b, 0x0d, 0x21, 0xb8, 0x7e, 0xa7, 0x66, 0x82, 0x6e, 0x5b, 0x62, 0x33, 0xa1, 0xb8, 0x39,
|
||||||
0x12, 0x42, 0x76, 0x56, 0x71, 0x41, 0x7e, 0x5e, 0x71, 0xaa, 0x90, 0x1a, 0x17, 0x1b, 0x44, 0x04,
|
0x16, 0x8b, 0xb5, 0x90, 0x40, 0xeb, 0xb6, 0xae, 0x5e, 0x49, 0x1b, 0xd7, 0x6e, 0xef, 0x27, 0x74,
|
||||||
0xec, 0x26, 0x6e, 0x23, 0x3e, 0x3d, 0xa8, 0x67, 0xa0, 0xea, 0xa0, 0xb2, 0x4e, 0x76, 0x27, 0x1e,
|
0xc7, 0xb6, 0x79, 0x24, 0x3d, 0xec, 0x31, 0x25, 0xb5, 0x59, 0x4d, 0xd4, 0x93, 0x90, 0xb4, 0x61,
|
||||||
0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x8c, 0xc7, 0x72, 0x0c, 0x51,
|
0x89, 0x5b, 0xe5, 0x33, 0x6c, 0xcc, 0x69, 0xb3, 0x98, 0x61, 0x63, 0xde, 0xff, 0x44, 0x98, 0xb8,
|
||||||
0x3a, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xfa, 0x19, 0x89, 0xc5, 0x19,
|
0x9b, 0xea, 0x54, 0x49, 0x2d, 0xc8, 0x11, 0x6e, 0x14, 0x8d, 0x5d, 0xd3, 0x1b, 0xee, 0x05, 0xe5,
|
||||||
0x99, 0xc9, 0xf9, 0x45, 0x05, 0xa0, 0xa0, 0x28, 0x2e, 0xcd, 0xd1, 0x47, 0x0f, 0xce, 0x24, 0x36,
|
0xe7, 0x97, 0x5e, 0x49, 0xc9, 0x31, 0x6e, 0xb2, 0x11, 0x57, 0x0a, 0xb4, 0xdd, 0xda, 0x1b, 0xb6,
|
||||||
0xb0, 0x88, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x46, 0x7e, 0xde, 0xed, 0x69, 0x01, 0x00, 0x00,
|
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) {
|
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 = encodeVarintAutoConfig(dAtA, i, uint64(len(m.ConsulToken)))
|
||||||
i += copy(dAtA[i:], 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 {
|
if m.XXX_unrecognized != nil {
|
||||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||||
}
|
}
|
||||||
|
@ -264,6 +318,41 @@ func (m *AutoConfigResponse) MarshalTo(dAtA []byte) (int, error) {
|
||||||
}
|
}
|
||||||
i += n1
|
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 {
|
if m.XXX_unrecognized != nil {
|
||||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||||
}
|
}
|
||||||
|
@ -305,6 +394,10 @@ func (m *AutoConfigRequest) Size() (n int) {
|
||||||
if l > 0 {
|
if l > 0 {
|
||||||
n += 1 + l + sovAutoConfig(uint64(l))
|
n += 1 + l + sovAutoConfig(uint64(l))
|
||||||
}
|
}
|
||||||
|
l = len(m.CSR)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovAutoConfig(uint64(l))
|
||||||
|
}
|
||||||
if m.XXX_unrecognized != nil {
|
if m.XXX_unrecognized != nil {
|
||||||
n += len(m.XXX_unrecognized)
|
n += len(m.XXX_unrecognized)
|
||||||
}
|
}
|
||||||
|
@ -321,6 +414,20 @@ func (m *AutoConfigResponse) Size() (n int) {
|
||||||
l = m.Config.Size()
|
l = m.Config.Size()
|
||||||
n += 1 + l + sovAutoConfig(uint64(l))
|
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 {
|
if m.XXX_unrecognized != nil {
|
||||||
n += len(m.XXX_unrecognized)
|
n += len(m.XXX_unrecognized)
|
||||||
}
|
}
|
||||||
|
@ -529,6 +636,38 @@ func (m *AutoConfigRequest) Unmarshal(dAtA []byte) error {
|
||||||
}
|
}
|
||||||
m.ConsulToken = string(dAtA[iNdEx:postIndex])
|
m.ConsulToken = string(dAtA[iNdEx:postIndex])
|
||||||
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:
|
default:
|
||||||
iNdEx = preIndex
|
iNdEx = preIndex
|
||||||
skippy, err := skipAutoConfig(dAtA[iNdEx:])
|
skippy, err := skipAutoConfig(dAtA[iNdEx:])
|
||||||
|
@ -619,6 +758,110 @@ func (m *AutoConfigResponse) Unmarshal(dAtA []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
iNdEx = postIndex
|
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:
|
default:
|
||||||
iNdEx = preIndex
|
iNdEx = preIndex
|
||||||
skippy, err := skipAutoConfig(dAtA[iNdEx:])
|
skippy, err := skipAutoConfig(dAtA[iNdEx:])
|
||||||
|
|
|
@ -5,6 +5,7 @@ package autoconf;
|
||||||
option go_package = "github.com/hashicorp/consul/proto/pbautoconf";
|
option go_package = "github.com/hashicorp/consul/proto/pbautoconf";
|
||||||
|
|
||||||
import "proto/pbconfig/config.proto";
|
import "proto/pbconfig/config.proto";
|
||||||
|
import "proto/pbconnect/connect.proto";
|
||||||
|
|
||||||
// AutoConfigRequest is the data structure to be sent along with the
|
// AutoConfigRequest is the data structure to be sent along with the
|
||||||
// AutoConfig.InitialConfiguration RPC
|
// AutoConfig.InitialConfiguration RPC
|
||||||
|
@ -28,9 +29,23 @@ message AutoConfigRequest {
|
||||||
// ConsulToken is a Consul ACL token that the agent requesting the
|
// ConsulToken is a Consul ACL token that the agent requesting the
|
||||||
// configuration already has.
|
// configuration already has.
|
||||||
string ConsulToken = 6;
|
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
|
// AutoConfigResponse is the data structure sent in response to a AutoConfig.InitialConfiguration request
|
||||||
message AutoConfigResponse {
|
message AutoConfigResponse {
|
||||||
|
// Config is the partial Consul configuration to inject into the agents own configuration
|
||||||
config.Config Config = 1;
|
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;
|
||||||
}
|
}
|
|
@ -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
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue