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.
|
||||
a.config = config
|
||||
|
||||
// create the cache using the rate limiting settings from the config. Note that this means
|
||||
// that these limits are not reloadable.
|
||||
a.cache = cache.New(a.config.Cache)
|
||||
|
||||
if flat.logger == nil {
|
||||
logConf := &logging.Config{
|
||||
LogLevel: config.LogLevel,
|
||||
|
@ -526,14 +530,34 @@ func New(options ...AgentOption) (*Agent, error) {
|
|||
return nil, fmt.Errorf("Failed to setup node ID: %v", err)
|
||||
}
|
||||
|
||||
acOpts := []autoconf.Option{
|
||||
autoconf.WithDirectRPC(a.connPool),
|
||||
autoconf.WithTLSConfigurator(a.tlsConfigurator),
|
||||
autoconf.WithBuilderOpts(flat.builderOpts),
|
||||
autoconf.WithLogger(a.logger),
|
||||
autoconf.WithOverrides(flat.overrides...),
|
||||
// We used to do this in the Start method. However it doesn't need to go
|
||||
// there any longer. Originally it did because we passed the agent
|
||||
// delegate to some of the cache registrations. Now we just
|
||||
// pass the agent itself so its safe to move here.
|
||||
a.registerCache()
|
||||
|
||||
cmConf := new(certmon.Config).
|
||||
WithCache(a.cache).
|
||||
WithTLSConfigurator(a.tlsConfigurator).
|
||||
WithDNSSANs(a.config.AutoConfig.DNSSANs).
|
||||
WithIPSANs(a.config.AutoConfig.IPSANs).
|
||||
WithDatacenter(a.config.Datacenter).
|
||||
WithNodeName(a.config.NodeName).
|
||||
WithFallback(a.autoConfigFallbackTLS).
|
||||
WithLogger(a.logger.Named(logging.AutoConfig)).
|
||||
WithTokens(a.tokens)
|
||||
acCertMon, err := certmon.New(cmConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ac, err := autoconf.New(acOpts...)
|
||||
|
||||
acConf := new(autoconf.Config).
|
||||
WithDirectRPC(a.connPool).
|
||||
WithBuilderOpts(flat.builderOpts).
|
||||
WithLogger(a.logger).
|
||||
WithOverrides(flat.overrides...).
|
||||
WithCertMonitor(acCertMon)
|
||||
ac, err := autoconf.New(acConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -664,9 +688,6 @@ func (a *Agent) Start(ctx context.Context) error {
|
|||
// regular and on-demand state synchronizations (anti-entropy).
|
||||
a.sync = ae.NewStateSyncer(a.State, c.AEInterval, a.shutdownCh, a.logger)
|
||||
|
||||
// create the cache
|
||||
a.cache = cache.New(c.Cache)
|
||||
|
||||
// create the config for the rpc server/client
|
||||
consulCfg, err := a.consulConfig()
|
||||
if err != nil {
|
||||
|
@ -715,10 +736,6 @@ func (a *Agent) Start(ctx context.Context) error {
|
|||
a.State.Delegate = a.delegate
|
||||
a.State.TriggerSyncChanges = a.sync.SyncChanges.Trigger
|
||||
|
||||
// Register the cache. We do this much later so the delegate is
|
||||
// populated from above.
|
||||
a.registerCache()
|
||||
|
||||
if a.config.AutoEncryptTLS && !a.config.ServerMode {
|
||||
reply, err := a.autoEncryptInitialCertificate(ctx)
|
||||
if err != nil {
|
||||
|
@ -756,6 +773,9 @@ func (a *Agent) Start(ctx context.Context) error {
|
|||
a.logger.Info("automatically upgraded to TLS")
|
||||
}
|
||||
|
||||
if err := a.autoConf.Start(&lib.StopChannelContext{StopCh: a.shutdownCh}); err != nil {
|
||||
return fmt.Errorf("AutoConf failed to start certificate monitor: %w", err)
|
||||
}
|
||||
a.serviceManager.Start()
|
||||
|
||||
// Load checks/services/metadata.
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
func (a *Agent) autoConfigFallbackTLS(ctx context.Context) (*structs.SignedResponse, error) {
|
||||
return a.autoConf.FallbackTLS(ctx)
|
||||
}
|
||||
|
||||
func (a *Agent) listenAndServeGRPC() error {
|
||||
if len(a.config.GRPCAddrs) < 1 {
|
||||
return nil
|
||||
|
@ -1822,6 +1846,16 @@ func (a *Agent) ShutdownAgent() error {
|
|||
// Stop the watches to avoid any notification/state change during shutdown
|
||||
a.stopAllWatches()
|
||||
|
||||
// this would be cancelled anyways (by the closing of the shutdown ch) but
|
||||
// this should help them to be stopped more quickly
|
||||
a.autoConf.Stop()
|
||||
|
||||
if a.certMonitor != nil {
|
||||
// this would be cancelled anyways (by the closing of the shutdown ch)
|
||||
// but this should help them to be stopped more quickly
|
||||
a.certMonitor.Stop()
|
||||
}
|
||||
|
||||
// Stop the service manager (must happen before we take the stateLock to avoid deadlock)
|
||||
if a.serviceManager != nil {
|
||||
a.serviceManager.Stop()
|
||||
|
|
|
@ -4651,7 +4651,8 @@ func TestAutoConfig_Integration(t *testing.T) {
|
|||
// eventually this test should really live with integration tests
|
||||
// the goal here is to have one test server and another test client
|
||||
// spin up both agents and allow the server to authorize the auto config
|
||||
// request and then see the client joined
|
||||
// request and then see the client joined. Finally we force a CA roots
|
||||
// update and wait to see that the agents TLS certificate gets updated.
|
||||
|
||||
cfgDir := testutil.TempDir(t, "auto-config")
|
||||
|
||||
|
@ -4689,7 +4690,6 @@ func TestAutoConfig_Integration(t *testing.T) {
|
|||
cert_file = "` + certFile + `"
|
||||
key_file = "` + keyFile + `"
|
||||
connect { enabled = true }
|
||||
auto_encrypt { allow_tls = true }
|
||||
auto_config {
|
||||
authorization {
|
||||
enabled = true
|
||||
|
@ -4746,11 +4746,44 @@ func TestAutoConfig_Integration(t *testing.T) {
|
|||
|
||||
defer client.Shutdown()
|
||||
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
require.NotNil(r, client.Agent.tlsConfigurator.Cert())
|
||||
})
|
||||
|
||||
// when this is successful we managed to get the gossip key and serf addresses to bind to
|
||||
// and then connect. Additionally we would have to have certificates or else the
|
||||
// verify_incoming config on the server would not let it work.
|
||||
testrpc.WaitForTestAgent(t, client.RPC, "dc1", testrpc.WithToken(TestDefaultMasterToken))
|
||||
|
||||
// grab the existing cert
|
||||
cert1 := client.Agent.tlsConfigurator.Cert()
|
||||
require.NotNil(t, cert1)
|
||||
|
||||
// force a roots rotation by updating the CA config
|
||||
t.Logf("Forcing roots rotation on the server")
|
||||
ca := connect.TestCA(t, nil)
|
||||
req := &structs.CARequest{
|
||||
Datacenter: "dc1",
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
Config: &structs.CAConfiguration{
|
||||
Provider: "consul",
|
||||
Config: map[string]interface{}{
|
||||
"LeafCertTTL": "1h",
|
||||
"PrivateKey": ca.SigningKey,
|
||||
"RootCert": ca.RootCert,
|
||||
"RotationPeriod": "6h",
|
||||
"IntermediateCertTTL": "3h",
|
||||
},
|
||||
},
|
||||
}
|
||||
var reply interface{}
|
||||
require.NoError(t, srv.RPC("ConnectCA.ConfigurationSet", &req, &reply))
|
||||
|
||||
// ensure that a new cert gets generated and pushed into the TLS configurator
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
require.NotEqual(r, cert1, client.Agent.tlsConfigurator.Cert())
|
||||
})
|
||||
|
||||
// spot check that we now have an ACL token
|
||||
require.NotEmpty(t, client.tokens.AgentToken())
|
||||
}
|
||||
|
|
|
@ -13,84 +13,38 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||
"github.com/hashicorp/consul/tlsutil"
|
||||
"github.com/hashicorp/go-discover"
|
||||
discoverk8s "github.com/hashicorp/go-discover/provider/k8s"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
)
|
||||
|
||||
const (
|
||||
// autoConfigFileName is the name of the file that the agent auto-config settings are
|
||||
// stored in within the data directory
|
||||
autoConfigFileName = "auto-config.json"
|
||||
|
||||
dummyTrustDomain = "dummytrustdomain"
|
||||
)
|
||||
|
||||
// DirectRPC is the interface that needs to be satisifed for AutoConfig to be able to perform
|
||||
// direct RPCs against individual servers. This should not use
|
||||
type DirectRPC interface {
|
||||
RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error
|
||||
}
|
||||
|
||||
type options struct {
|
||||
logger hclog.Logger
|
||||
directRPC DirectRPC
|
||||
tlsConfigurator *tlsutil.Configurator
|
||||
builderOpts config.BuilderOpts
|
||||
waiter *lib.RetryWaiter
|
||||
overrides []config.Source
|
||||
}
|
||||
|
||||
// Option represents one point of configurability for the New function
|
||||
// when creating a new AutoConfig object
|
||||
type Option func(*options)
|
||||
|
||||
// WithLogger will cause the created AutoConfig type to use the provided logger
|
||||
func WithLogger(logger hclog.Logger) Option {
|
||||
return func(opt *options) {
|
||||
opt.logger = logger
|
||||
var (
|
||||
pbMarshaler = &jsonpb.Marshaler{
|
||||
OrigName: false,
|
||||
EnumsAsInts: false,
|
||||
Indent: " ",
|
||||
EmitDefaults: true,
|
||||
}
|
||||
}
|
||||
|
||||
// WithTLSConfigurator will cause the created AutoConfig type to use the provided configurator
|
||||
func WithTLSConfigurator(tlsConfigurator *tlsutil.Configurator) Option {
|
||||
return func(opt *options) {
|
||||
opt.tlsConfigurator = tlsConfigurator
|
||||
pbUnmarshaler = &jsonpb.Unmarshaler{
|
||||
AllowUnknownFields: false,
|
||||
}
|
||||
}
|
||||
|
||||
// WithConnectionPool will cause the created AutoConfig type to use the provided connection pool
|
||||
func WithDirectRPC(directRPC DirectRPC) Option {
|
||||
return func(opt *options) {
|
||||
opt.directRPC = directRPC
|
||||
}
|
||||
}
|
||||
|
||||
// WithBuilderOpts will cause the created AutoConfig type to use the provided CLI builderOpts
|
||||
func WithBuilderOpts(builderOpts config.BuilderOpts) Option {
|
||||
return func(opt *options) {
|
||||
opt.builderOpts = builderOpts
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryWaiter will cause the created AutoConfig type to use the provided retry waiter
|
||||
func WithRetryWaiter(waiter *lib.RetryWaiter) Option {
|
||||
return func(opt *options) {
|
||||
opt.waiter = waiter
|
||||
}
|
||||
}
|
||||
|
||||
// WithOverrides is used to provide a config source to append to the tail sources
|
||||
// during config building. It is really only useful for testing to tune non-user
|
||||
// configurable tunables to make various tests converge more quickly than they
|
||||
// could otherwise.
|
||||
func WithOverrides(overrides ...config.Source) Option {
|
||||
return func(opt *options) {
|
||||
opt.overrides = overrides
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// AutoConfig is all the state necessary for being able to parse a configuration
|
||||
// as well as perform the necessary RPCs to perform Agent Auto Configuration.
|
||||
|
@ -101,86 +55,52 @@ func WithOverrides(overrides ...config.Source) Option {
|
|||
// then we will need to add some locking here. I am deferring that for now
|
||||
// to help ease the review of this already large PR.
|
||||
type AutoConfig struct {
|
||||
config *config.RuntimeConfig
|
||||
builderOpts config.BuilderOpts
|
||||
logger hclog.Logger
|
||||
directRPC DirectRPC
|
||||
tlsConfigurator *tlsutil.Configurator
|
||||
autoConfigData string
|
||||
waiter *lib.RetryWaiter
|
||||
overrides []config.Source
|
||||
}
|
||||
|
||||
func flattenOptions(opts []Option) options {
|
||||
var flat options
|
||||
for _, opt := range opts {
|
||||
opt(&flat)
|
||||
}
|
||||
return flat
|
||||
builderOpts config.BuilderOpts
|
||||
logger hclog.Logger
|
||||
directRPC DirectRPC
|
||||
waiter *lib.RetryWaiter
|
||||
overrides []config.Source
|
||||
certMonitor CertMonitor
|
||||
config *config.RuntimeConfig
|
||||
autoConfigData string
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a new AutoConfig object for providing automatic
|
||||
// Consul configuration.
|
||||
func New(options ...Option) (*AutoConfig, error) {
|
||||
flat := flattenOptions(options)
|
||||
func New(config *Config) (*AutoConfig, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("must provide a config struct")
|
||||
}
|
||||
|
||||
if flat.directRPC == nil {
|
||||
if config.DirectRPC == nil {
|
||||
return nil, fmt.Errorf("must provide a direct RPC delegate")
|
||||
}
|
||||
|
||||
if flat.tlsConfigurator == nil {
|
||||
return nil, fmt.Errorf("must provide a TLS configurator")
|
||||
}
|
||||
|
||||
logger := flat.logger
|
||||
logger := config.Logger
|
||||
if logger == nil {
|
||||
logger = hclog.NewNullLogger()
|
||||
} else {
|
||||
logger = logger.Named(logging.AutoConfig)
|
||||
}
|
||||
|
||||
waiter := flat.waiter
|
||||
waiter := config.Waiter
|
||||
if waiter == nil {
|
||||
waiter = lib.NewRetryWaiter(1, 0, 10*time.Minute, lib.NewJitterRandomStagger(25))
|
||||
}
|
||||
|
||||
ac := &AutoConfig{
|
||||
builderOpts: flat.builderOpts,
|
||||
logger: logger,
|
||||
directRPC: flat.directRPC,
|
||||
tlsConfigurator: flat.tlsConfigurator,
|
||||
waiter: waiter,
|
||||
overrides: flat.overrides,
|
||||
builderOpts: config.BuilderOpts,
|
||||
logger: logger,
|
||||
directRPC: config.DirectRPC,
|
||||
waiter: waiter,
|
||||
overrides: config.Overrides,
|
||||
certMonitor: config.CertMonitor,
|
||||
}
|
||||
|
||||
return ac, nil
|
||||
}
|
||||
|
||||
// LoadConfig will build the configuration including the extraHead source injected
|
||||
// after all other defaults but before any user supplied configuration and the overrides
|
||||
// source injected as the final source in the configuration parsing chain.
|
||||
func LoadConfig(builderOpts config.BuilderOpts, extraHead config.Source, overrides ...config.Source) (*config.RuntimeConfig, []string, error) {
|
||||
b, err := config.NewBuilder(builderOpts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if extraHead.Data != "" {
|
||||
b.Head = append(b.Head, extraHead)
|
||||
}
|
||||
|
||||
if len(overrides) != 0 {
|
||||
b.Tail = append(b.Tail, overrides...)
|
||||
}
|
||||
|
||||
cfg, err := b.BuildAndValidate()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &cfg, b.Warnings, nil
|
||||
}
|
||||
|
||||
// ReadConfig will parse the current configuration and inject any
|
||||
// auto-config sources if present into the correct place in the parsing chain.
|
||||
func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) {
|
||||
|
@ -219,8 +139,18 @@ func (ac *AutoConfig) restorePersistedAutoConfig() (bool, error) {
|
|||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err == nil {
|
||||
rdr := strings.NewReader(string(content))
|
||||
|
||||
var resp pbautoconf.AutoConfigResponse
|
||||
if err := pbUnmarshaler.Unmarshal(rdr, &resp); err != nil {
|
||||
return false, fmt.Errorf("failed to decode persisted auto-config data: %w", err)
|
||||
}
|
||||
|
||||
if err := ac.update(&resp); err != nil {
|
||||
return false, fmt.Errorf("error restoring persisted auto-config response: %w", err)
|
||||
}
|
||||
|
||||
ac.logger.Info("restored persisted configuration", "path", path)
|
||||
ac.autoConfigData = string(content)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
@ -305,12 +235,12 @@ func (ac *AutoConfig) introToken() (string, error) {
|
|||
return token, nil
|
||||
}
|
||||
|
||||
// autoConfigHosts is responsible for taking the list of server addresses and
|
||||
// serverHosts is responsible for taking the list of server addresses and
|
||||
// resolving any go-discover provider invocations. It will then return a list
|
||||
// of hosts. These might be hostnames and is expected that DNS resolution may
|
||||
// be performed after this function runs. Additionally these may contain ports
|
||||
// so SplitHostPort could also be necessary.
|
||||
func (ac *AutoConfig) autoConfigHosts() ([]string, error) {
|
||||
func (ac *AutoConfig) serverHosts() ([]string, error) {
|
||||
servers := ac.config.AutoConfig.ServerAddresses
|
||||
|
||||
providers := make(map[string]discover.Provider)
|
||||
|
@ -388,31 +318,20 @@ func (ac *AutoConfig) resolveHost(hostPort string) []net.TCPAddr {
|
|||
return addrs
|
||||
}
|
||||
|
||||
// recordAutoConfigReply takes an AutoConfig RPC reply records it with the agent
|
||||
// recordResponse takes an AutoConfig RPC response records it with the agent
|
||||
// This will persist the configuration to disk (unless in dev mode running without
|
||||
// a data dir) and will reload the configuration.
|
||||
func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse) error {
|
||||
// overwrite the auto encrypt DNS SANs with the ones specified in the auto_config stanza
|
||||
if len(ac.config.AutoConfig.DNSSANs) > 0 && reply.Config.AutoEncrypt != nil {
|
||||
reply.Config.AutoEncrypt.DNSSAN = ac.config.AutoConfig.DNSSANs
|
||||
}
|
||||
|
||||
// overwrite the auto encrypt IP SANs with the ones specified in the auto_config stanza
|
||||
if len(ac.config.AutoConfig.IPSANs) > 0 && reply.Config.AutoEncrypt != nil {
|
||||
var ips []string
|
||||
for _, ip := range ac.config.AutoConfig.IPSANs {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
reply.Config.AutoEncrypt.IPSAN = ips
|
||||
}
|
||||
|
||||
conf, err := json.Marshal(translateConfig(reply.Config))
|
||||
func (ac *AutoConfig) recordResponse(resp *pbautoconf.AutoConfigResponse) error {
|
||||
serialized, err := pbMarshaler.MarshalToString(resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode auto-config configuration as JSON: %w", err)
|
||||
return fmt.Errorf("failed to encode auto-config response as JSON: %w", err)
|
||||
}
|
||||
|
||||
ac.autoConfigData = string(conf)
|
||||
if err := ac.update(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now that we know the configuration is generally fine including TLS certs go ahead and persist it to disk.
|
||||
if ac.config.DataDir == "" {
|
||||
ac.logger.Debug("not persisting auto-config settings because there is no data directory")
|
||||
return nil
|
||||
|
@ -420,7 +339,7 @@ func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse
|
|||
|
||||
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
|
||||
|
||||
err = ioutil.WriteFile(path, conf, 0660)
|
||||
err = ioutil.WriteFile(path, []byte(serialized), 0660)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write auto-config configurations: %w", err)
|
||||
}
|
||||
|
@ -435,10 +354,10 @@ func (ac *AutoConfig) recordAutoConfigReply(reply *pbautoconf.AutoConfigResponse
|
|||
// successful the bool return will be true and the err value will indicate whether we
|
||||
// successfully recorded the auto config settings (persisted to disk and stored internally
|
||||
// on the AutoConfig object)
|
||||
func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context) (bool, error) {
|
||||
func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context, csr string, key string) (*pbautoconf.AutoConfigResponse, error) {
|
||||
token, err := ac.introToken()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request := pbautoconf.AutoConfigRequest{
|
||||
|
@ -446,50 +365,63 @@ func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context) (bool, er
|
|||
Node: ac.config.NodeName,
|
||||
Segment: ac.config.SegmentName,
|
||||
JWT: token,
|
||||
CSR: csr,
|
||||
}
|
||||
|
||||
var reply pbautoconf.AutoConfigResponse
|
||||
var resp pbautoconf.AutoConfigResponse
|
||||
|
||||
servers, err := ac.autoConfigHosts()
|
||||
servers, err := ac.serverHosts()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
// try each IP to see if we can successfully make the request
|
||||
for _, addr := range ac.resolveHost(s) {
|
||||
if ctx.Err() != nil {
|
||||
return false, ctx.Err()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
ac.logger.Debug("making AutoConfig.InitialConfiguration RPC", "addr", addr.String())
|
||||
if err = ac.directRPC.RPC(ac.config.Datacenter, ac.config.NodeName, &addr, "AutoConfig.InitialConfiguration", &request, &reply); err != nil {
|
||||
if err = ac.directRPC.RPC(ac.config.Datacenter, ac.config.NodeName, &addr, "AutoConfig.InitialConfiguration", &request, &resp); err != nil {
|
||||
ac.logger.Error("AutoConfig.InitialConfiguration RPC failed", "addr", addr.String(), "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
return true, ac.recordAutoConfigReply(&reply)
|
||||
// update the Certificate with the private key we generated locally
|
||||
if resp.Certificate != nil {
|
||||
resp.Certificate.PrivateKeyPEM = key
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, ctx.Err()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// getInitialConfiguration implements a loop to retry calls to getInitialConfigurationOnce.
|
||||
// It uses the RetryWaiter on the AutoConfig object to control how often to attempt
|
||||
// the initial configuration process. It is also canceallable by cancelling the provided context.
|
||||
func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) error {
|
||||
// generate a CSR
|
||||
csr, key, err := ac.generateCSR()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this resets the failures so that we will perform immediate request
|
||||
wait := ac.waiter.Success()
|
||||
for {
|
||||
select {
|
||||
case <-wait:
|
||||
done, err := ac.getInitialConfigurationOnce(ctx)
|
||||
if done {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
resp, err := ac.getInitialConfigurationOnce(ctx, csr, key)
|
||||
if resp != nil {
|
||||
return ac.recordResponse(resp)
|
||||
} else if err != nil {
|
||||
ac.logger.Error(err.Error())
|
||||
} else {
|
||||
ac.logger.Error("No error returned when fetching the initial auto-configuration but no response was either")
|
||||
}
|
||||
wait = ac.waiter.Failed()
|
||||
case <-ctx.Done():
|
||||
|
@ -498,3 +430,164 @@ func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateCSR will generate a CSR for an Agent certificate. This should
|
||||
// be sent along with the AutoConfig.InitialConfiguration RPC. The generated
|
||||
// CSR does NOT have a real trust domain as when generating this we do
|
||||
// not yet have the CA roots. The server will update the trust domain
|
||||
// for us though.
|
||||
func (ac *AutoConfig) generateCSR() (csr string, key string, err error) {
|
||||
// We don't provide the correct host here, because we don't know any
|
||||
// better at this point. Apart from the domain, we would need the
|
||||
// ClusterID, which we don't have. This is why we go with
|
||||
// dummyTrustDomain the first time. Subsequent CSRs will have the
|
||||
// correct TrustDomain.
|
||||
id := &connect.SpiffeIDAgent{
|
||||
// will be replaced
|
||||
Host: dummyTrustDomain,
|
||||
Datacenter: ac.config.Datacenter,
|
||||
Agent: ac.config.NodeName,
|
||||
}
|
||||
|
||||
caConfig, err := ac.config.ConnectCAConfiguration()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Cannot generate CSR: %w", err)
|
||||
}
|
||||
|
||||
conf, err := caConfig.GetCommonConfig()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Failed to load common CA configuration: %w", err)
|
||||
}
|
||||
|
||||
if conf.PrivateKeyType == "" {
|
||||
conf.PrivateKeyType = connect.DefaultPrivateKeyType
|
||||
}
|
||||
if conf.PrivateKeyBits == 0 {
|
||||
conf.PrivateKeyBits = connect.DefaultPrivateKeyBits
|
||||
}
|
||||
|
||||
// Create a new private key
|
||||
pk, pkPEM, err := connect.GeneratePrivateKeyWithConfig(conf.PrivateKeyType, conf.PrivateKeyBits)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Failed to generate private key: %w", err)
|
||||
}
|
||||
|
||||
dnsNames := append([]string{"localhost"}, ac.config.AutoConfig.DNSSANs...)
|
||||
ipAddresses := append([]net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::")}, ac.config.AutoConfig.IPSANs...)
|
||||
|
||||
// Create a CSR.
|
||||
//
|
||||
// The Common Name includes the dummy trust domain for now but Server will
|
||||
// override this when it is signed anyway so it's OK.
|
||||
cn := connect.AgentCN(ac.config.NodeName, dummyTrustDomain)
|
||||
csr, err = connect.CreateCSR(id, cn, pk, dnsNames, ipAddresses)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return csr, pkPEM, nil
|
||||
}
|
||||
|
||||
// update will take an AutoConfigResponse and do all things necessary
|
||||
// to restore those settings. This currently involves updating the
|
||||
// config data to be used during a call to ReadConfig, updating the
|
||||
// tls Configurator and prepopulating the cache.
|
||||
func (ac *AutoConfig) update(resp *pbautoconf.AutoConfigResponse) error {
|
||||
if err := ac.updateConfigFromResponse(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ac.updateTLSFromResponse(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateConfigFromResponse is responsible for generating the JSON compatible with the
|
||||
// agent/config.Config struct
|
||||
func (ac *AutoConfig) updateConfigFromResponse(resp *pbautoconf.AutoConfigResponse) error {
|
||||
// here we want to serialize the translated configuration for use in injecting into the normal
|
||||
// configuration parsing chain.
|
||||
conf, err := json.Marshal(translateConfig(resp.Config))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode auto-config configuration as JSON: %w", err)
|
||||
}
|
||||
|
||||
ac.autoConfigData = string(conf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateTLSFromResponse will update the TLS certificate and roots in the shared
|
||||
// TLS configurator.
|
||||
func (ac *AutoConfig) updateTLSFromResponse(resp *pbautoconf.AutoConfigResponse) error {
|
||||
if ac.certMonitor == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
roots, err := translateCARootsToStructs(resp.CARoots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := translateIssuedCertToStructs(resp.Certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
update := &structs.SignedResponse{
|
||||
IssuedCert: *cert,
|
||||
ConnectCARoots: *roots,
|
||||
ManualCARoots: resp.ExtraCACertificates,
|
||||
}
|
||||
|
||||
if resp.Config != nil && resp.Config.TLS != nil {
|
||||
update.VerifyServerHostname = resp.Config.TLS.VerifyServerHostname
|
||||
}
|
||||
|
||||
if err := ac.certMonitor.Update(update); err != nil {
|
||||
return fmt.Errorf("failed to update the certificate monitor: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ac *AutoConfig) Start(ctx context.Context) error {
|
||||
if ac.certMonitor == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ac.config.AutoConfig.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := ac.certMonitor.Start(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ac *AutoConfig) Stop() bool {
|
||||
if ac.certMonitor == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !ac.config.AutoConfig.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
return ac.certMonitor.Stop()
|
||||
}
|
||||
|
||||
func (ac *AutoConfig) FallbackTLS(ctx context.Context) (*structs.SignedResponse, error) {
|
||||
// generate a CSR
|
||||
csr, key, err := ac.generateCSR()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := ac.getInitialConfigurationOnce(ctx, csr, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extractSignedResponse(resp)
|
||||
}
|
||||
|
|
|
@ -2,21 +2,23 @@ package autoconf
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||
"github.com/hashicorp/consul/proto/pbconfig"
|
||||
"github.com/hashicorp/consul/proto/pbconnect"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/hashicorp/consul/tlsutil"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -26,7 +28,17 @@ type mockDirectRPC struct {
|
|||
}
|
||||
|
||||
func (m *mockDirectRPC) RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error {
|
||||
retValues := m.Called(dc, node, addr, method, args, reply)
|
||||
var retValues mock.Arguments
|
||||
if method == "AutoConfig.InitialConfiguration" {
|
||||
req := args.(*pbautoconf.AutoConfigRequest)
|
||||
csr := req.CSR
|
||||
req.CSR = ""
|
||||
retValues = m.Called(dc, node, addr, method, args, reply)
|
||||
req.CSR = csr
|
||||
} else {
|
||||
retValues = m.Called(dc, node, addr, method, args, reply)
|
||||
}
|
||||
|
||||
switch ret := retValues.Get(0).(type) {
|
||||
case error:
|
||||
return ret
|
||||
|
@ -38,30 +50,48 @@ func (m *mockDirectRPC) RPC(dc string, node string, addr net.Addr, method string
|
|||
}
|
||||
}
|
||||
|
||||
type mockCertMonitor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockCertMonitor) Start(_ context.Context) (<-chan struct{}, error) {
|
||||
ret := m.Called()
|
||||
ch := ret.Get(0).(<-chan struct{})
|
||||
return ch, ret.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockCertMonitor) Stop() bool {
|
||||
return m.Called().Bool(0)
|
||||
}
|
||||
|
||||
func (m *mockCertMonitor) Update(resp *structs.SignedResponse) error {
|
||||
var privKey string
|
||||
// filter out real certificates as we cannot predict their values
|
||||
if resp != nil && strings.HasPrefix(resp.IssuedCert.PrivateKeyPEM, "-----BEGIN") {
|
||||
privKey = resp.IssuedCert.PrivateKeyPEM
|
||||
resp.IssuedCert.PrivateKeyPEM = ""
|
||||
}
|
||||
err := m.Called(resp).Error(0)
|
||||
if privKey != "" {
|
||||
resp.IssuedCert.PrivateKeyPEM = privKey
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type testCase struct {
|
||||
opts []Option
|
||||
config Config
|
||||
err string
|
||||
validate func(t *testing.T, ac *AutoConfig)
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"no-direct-rpc": {
|
||||
opts: []Option{
|
||||
WithTLSConfigurator(&tlsutil.Configurator{}),
|
||||
},
|
||||
err: "must provide a direct RPC delegate",
|
||||
},
|
||||
"no-tls-configurator": {
|
||||
opts: []Option{
|
||||
WithDirectRPC(&mockDirectRPC{}),
|
||||
},
|
||||
err: "must provide a TLS configurator",
|
||||
},
|
||||
"ok": {
|
||||
opts: []Option{
|
||||
WithTLSConfigurator(&tlsutil.Configurator{}),
|
||||
WithDirectRPC(&mockDirectRPC{}),
|
||||
config: Config{
|
||||
DirectRPC: &mockDirectRPC{},
|
||||
},
|
||||
validate: func(t *testing.T, ac *AutoConfig) {
|
||||
t.Helper()
|
||||
|
@ -72,7 +102,7 @@ func TestNew(t *testing.T) {
|
|||
|
||||
for name, tcase := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ac, err := New(tcase.opts...)
|
||||
ac, err := New(&tcase.config)
|
||||
if tcase.err != "" {
|
||||
testutil.RequireErrorContains(t, err, tcase.err)
|
||||
} else {
|
||||
|
@ -173,7 +203,10 @@ func TestInitialConfiguration_disabled(t *testing.T) {
|
|||
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
||||
|
||||
directRPC := mockDirectRPC{}
|
||||
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
||||
conf := new(Config).
|
||||
WithBuilderOpts(builderOpts).
|
||||
WithDirectRPC(&directRPC)
|
||||
ac, err := New(conf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ac)
|
||||
|
||||
|
@ -208,7 +241,10 @@ func TestInitialConfiguration_cancelled(t *testing.T) {
|
|||
}
|
||||
|
||||
directRPC.On("RPC", "dc1", "autoconf", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300}, "AutoConfig.InitialConfiguration", &expectedRequest, mock.Anything).Return(fmt.Errorf("injected error")).Times(0)
|
||||
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
||||
conf := new(Config).
|
||||
WithBuilderOpts(builderOpts).
|
||||
WithDirectRPC(&directRPC)
|
||||
ac, err := New(conf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ac)
|
||||
|
||||
|
@ -235,26 +271,97 @@ func TestInitialConfiguration_restored(t *testing.T) {
|
|||
|
||||
// persist an auto config response to the data dir where it is expected
|
||||
persistedFile := filepath.Join(dataDir, autoConfigFileName)
|
||||
response := &pbconfig.Config{
|
||||
PrimaryDatacenter: "primary",
|
||||
response := &pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
PrimaryDatacenter: "primary",
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyServerHostname: true,
|
||||
},
|
||||
},
|
||||
CARoots: &pbconnect.CARoots{
|
||||
ActiveRootID: "active",
|
||||
TrustDomain: "trust",
|
||||
Roots: []*pbconnect.CARoot{
|
||||
{
|
||||
ID: "active",
|
||||
Name: "foo",
|
||||
SerialNumber: 42,
|
||||
SigningKeyID: "blarg",
|
||||
NotBefore: &types.Timestamp{Seconds: 5000, Nanos: 100},
|
||||
NotAfter: &types.Timestamp{Seconds: 10000, Nanos: 9009},
|
||||
RootCert: "not an actual cert",
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Certificate: &pbconnect.IssuedCert{
|
||||
SerialNumber: "1234",
|
||||
CertPEM: "not a cert",
|
||||
PrivateKeyPEM: "private",
|
||||
Agent: "foo",
|
||||
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
|
||||
ValidAfter: &types.Timestamp{Seconds: 6000},
|
||||
ValidBefore: &types.Timestamp{Seconds: 7000},
|
||||
},
|
||||
ExtraCACertificates: []string{"blarg"},
|
||||
}
|
||||
data, err := json.Marshal(translateConfig(response))
|
||||
data, err := pbMarshaler.MarshalToString(response)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, ioutil.WriteFile(persistedFile, data, 0600))
|
||||
require.NoError(t, ioutil.WriteFile(persistedFile, []byte(data), 0600))
|
||||
|
||||
directRPC := mockDirectRPC{}
|
||||
|
||||
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
||||
// setup the mock certificate monitor to ensure that the initial state gets
|
||||
// updated appropriately during config restoration.
|
||||
certMon := mockCertMonitor{}
|
||||
certMon.On("Update", &structs.SignedResponse{
|
||||
IssuedCert: structs.IssuedCert{
|
||||
SerialNumber: "1234",
|
||||
CertPEM: "not a cert",
|
||||
PrivateKeyPEM: "private",
|
||||
Agent: "foo",
|
||||
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
|
||||
ValidAfter: time.Unix(6000, 0),
|
||||
ValidBefore: time.Unix(7000, 0),
|
||||
},
|
||||
ConnectCARoots: structs.IndexedCARoots{
|
||||
ActiveRootID: "active",
|
||||
TrustDomain: "trust",
|
||||
Roots: []*structs.CARoot{
|
||||
{
|
||||
ID: "active",
|
||||
Name: "foo",
|
||||
SerialNumber: 42,
|
||||
SigningKeyID: "blarg",
|
||||
NotBefore: time.Unix(5000, 100),
|
||||
NotAfter: time.Unix(10000, 9009),
|
||||
RootCert: "not an actual cert",
|
||||
Active: true,
|
||||
// the decoding process doesn't leave this nil
|
||||
IntermediateCerts: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
ManualCARoots: []string{"blarg"},
|
||||
VerifyServerHostname: true,
|
||||
}).Return(nil).Once()
|
||||
|
||||
conf := new(Config).
|
||||
WithBuilderOpts(builderOpts).
|
||||
WithDirectRPC(&directRPC).
|
||||
WithCertMonitor(&certMon)
|
||||
ac, err := New(conf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ac)
|
||||
|
||||
cfg, err := ac.InitialConfiguration(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err, data)
|
||||
require.NotNil(t, cfg)
|
||||
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
||||
|
||||
// ensure no RPC was made
|
||||
directRPC.AssertExpectations(t)
|
||||
certMon.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestInitialConfiguration_success(t *testing.T) {
|
||||
|
@ -275,7 +382,36 @@ func TestInitialConfiguration_success(t *testing.T) {
|
|||
require.True(t, ok)
|
||||
resp.Config = &pbconfig.Config{
|
||||
PrimaryDatacenter: "primary",
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyServerHostname: true,
|
||||
},
|
||||
}
|
||||
|
||||
resp.CARoots = &pbconnect.CARoots{
|
||||
ActiveRootID: "active",
|
||||
TrustDomain: "trust",
|
||||
Roots: []*pbconnect.CARoot{
|
||||
{
|
||||
ID: "active",
|
||||
Name: "foo",
|
||||
SerialNumber: 42,
|
||||
SigningKeyID: "blarg",
|
||||
NotBefore: &types.Timestamp{Seconds: 5000, Nanos: 100},
|
||||
NotAfter: &types.Timestamp{Seconds: 10000, Nanos: 9009},
|
||||
RootCert: "not an actual cert",
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp.Certificate = &pbconnect.IssuedCert{
|
||||
SerialNumber: "1234",
|
||||
CertPEM: "not a cert",
|
||||
Agent: "foo",
|
||||
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
|
||||
ValidAfter: &types.Timestamp{Seconds: 6000},
|
||||
ValidBefore: &types.Timestamp{Seconds: 7000},
|
||||
}
|
||||
resp.ExtraCACertificates = []string{"blarg"}
|
||||
}
|
||||
|
||||
expectedRequest := pbautoconf.AutoConfigRequest{
|
||||
|
@ -293,7 +429,44 @@ func TestInitialConfiguration_success(t *testing.T) {
|
|||
&expectedRequest,
|
||||
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
|
||||
|
||||
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
||||
// setup the mock certificate monitor to ensure that the initial state gets
|
||||
// updated appropriately during config restoration.
|
||||
certMon := mockCertMonitor{}
|
||||
certMon.On("Update", &structs.SignedResponse{
|
||||
IssuedCert: structs.IssuedCert{
|
||||
SerialNumber: "1234",
|
||||
CertPEM: "not a cert",
|
||||
PrivateKeyPEM: "", // the mock
|
||||
Agent: "foo",
|
||||
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
|
||||
ValidAfter: time.Unix(6000, 0),
|
||||
ValidBefore: time.Unix(7000, 0),
|
||||
},
|
||||
ConnectCARoots: structs.IndexedCARoots{
|
||||
ActiveRootID: "active",
|
||||
TrustDomain: "trust",
|
||||
Roots: []*structs.CARoot{
|
||||
{
|
||||
ID: "active",
|
||||
Name: "foo",
|
||||
SerialNumber: 42,
|
||||
SigningKeyID: "blarg",
|
||||
NotBefore: time.Unix(5000, 100),
|
||||
NotAfter: time.Unix(10000, 9009),
|
||||
RootCert: "not an actual cert",
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ManualCARoots: []string{"blarg"},
|
||||
VerifyServerHostname: true,
|
||||
}).Return(nil).Once()
|
||||
|
||||
conf := new(Config).
|
||||
WithBuilderOpts(builderOpts).
|
||||
WithDirectRPC(&directRPC).
|
||||
WithCertMonitor(&certMon)
|
||||
ac, err := New(conf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ac)
|
||||
|
||||
|
@ -307,6 +480,7 @@ func TestInitialConfiguration_success(t *testing.T) {
|
|||
|
||||
// ensure no RPC was made
|
||||
directRPC.AssertExpectations(t)
|
||||
certMon.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestInitialConfiguration_retries(t *testing.T) {
|
||||
|
@ -381,7 +555,11 @@ func TestInitialConfiguration_retries(t *testing.T) {
|
|||
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
|
||||
|
||||
waiter := lib.NewRetryWaiter(2, 0, 1*time.Millisecond, nil)
|
||||
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC), WithRetryWaiter(waiter))
|
||||
conf := new(Config).
|
||||
WithBuilderOpts(builderOpts).
|
||||
WithDirectRPC(&directRPC).
|
||||
WithRetryWaiter(waiter)
|
||||
ac, err := New(conf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ac)
|
||||
|
||||
|
@ -396,3 +574,162 @@ func TestInitialConfiguration_retries(t *testing.T) {
|
|||
// ensure no RPC was made
|
||||
directRPC.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestAutoConfig_StartStop(t *testing.T) {
|
||||
// currently the only thing running for autoconf is just the cert monitor
|
||||
// so this test only needs to ensure that the cert monitor is started and
|
||||
// stopped and not that anything with regards to running the cert monitor
|
||||
// actually work. Those are tested in the cert-monitor package.
|
||||
|
||||
_, configDir, builderOpts := testSetupAutoConf(t)
|
||||
|
||||
cfgFile := filepath.Join(configDir, "test.json")
|
||||
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
|
||||
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["198.18.0.1", "198.18.0.2:8398", "198.18.0.3:8399", "127.0.0.1:1234"]}, "verify_outgoing": true
|
||||
}`), 0600))
|
||||
|
||||
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
||||
directRPC := &mockDirectRPC{}
|
||||
certMon := &mockCertMonitor{}
|
||||
|
||||
certMon.On("Start").Return((<-chan struct{})(nil), nil).Once()
|
||||
certMon.On("Stop").Return(true).Once()
|
||||
|
||||
conf := new(Config).
|
||||
WithBuilderOpts(builderOpts).
|
||||
WithDirectRPC(directRPC).
|
||||
WithCertMonitor(certMon)
|
||||
|
||||
ac, err := New(conf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ac)
|
||||
cfg, err := ac.ReadConfig()
|
||||
require.NoError(t, err)
|
||||
ac.config = cfg
|
||||
|
||||
require.NoError(t, ac.Start(context.Background()))
|
||||
require.True(t, ac.Stop())
|
||||
|
||||
certMon.AssertExpectations(t)
|
||||
directRPC.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFallBackTLS(t *testing.T) {
|
||||
_, configDir, builderOpts := testSetupAutoConf(t)
|
||||
|
||||
cfgFile := filepath.Join(configDir, "test.json")
|
||||
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
|
||||
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["127.0.0.1:8300"]}, "verify_outgoing": true
|
||||
}`), 0600))
|
||||
|
||||
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
||||
|
||||
directRPC := mockDirectRPC{}
|
||||
|
||||
populateResponse := func(val interface{}) {
|
||||
resp, ok := val.(*pbautoconf.AutoConfigResponse)
|
||||
require.True(t, ok)
|
||||
resp.Config = &pbconfig.Config{
|
||||
PrimaryDatacenter: "primary",
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyServerHostname: true,
|
||||
},
|
||||
}
|
||||
|
||||
resp.CARoots = &pbconnect.CARoots{
|
||||
ActiveRootID: "active",
|
||||
TrustDomain: "trust",
|
||||
Roots: []*pbconnect.CARoot{
|
||||
{
|
||||
ID: "active",
|
||||
Name: "foo",
|
||||
SerialNumber: 42,
|
||||
SigningKeyID: "blarg",
|
||||
NotBefore: &types.Timestamp{Seconds: 5000, Nanos: 100},
|
||||
NotAfter: &types.Timestamp{Seconds: 10000, Nanos: 9009},
|
||||
RootCert: "not an actual cert",
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp.Certificate = &pbconnect.IssuedCert{
|
||||
SerialNumber: "1234",
|
||||
CertPEM: "not a cert",
|
||||
Agent: "foo",
|
||||
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
|
||||
ValidAfter: &types.Timestamp{Seconds: 6000},
|
||||
ValidBefore: &types.Timestamp{Seconds: 7000},
|
||||
}
|
||||
resp.ExtraCACertificates = []string{"blarg"}
|
||||
}
|
||||
|
||||
expectedRequest := pbautoconf.AutoConfigRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "autoconf",
|
||||
JWT: "blarg",
|
||||
}
|
||||
|
||||
directRPC.On(
|
||||
"RPC",
|
||||
"dc1",
|
||||
"autoconf",
|
||||
&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300},
|
||||
"AutoConfig.InitialConfiguration",
|
||||
&expectedRequest,
|
||||
&pbautoconf.AutoConfigResponse{}).Return(populateResponse)
|
||||
|
||||
// setup the mock certificate monitor we don't expect it to be used
|
||||
// as the FallbackTLS method is mainly used by the certificate monitor
|
||||
// if for some reason it fails to renew the TLS certificate in time.
|
||||
certMon := mockCertMonitor{}
|
||||
|
||||
conf := new(Config).
|
||||
WithBuilderOpts(builderOpts).
|
||||
WithDirectRPC(&directRPC).
|
||||
WithCertMonitor(&certMon)
|
||||
ac, err := New(conf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ac)
|
||||
ac.config, err = ac.ReadConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
actual, err := ac.FallbackTLS(context.Background())
|
||||
require.NoError(t, err)
|
||||
expected := &structs.SignedResponse{
|
||||
ConnectCARoots: structs.IndexedCARoots{
|
||||
ActiveRootID: "active",
|
||||
TrustDomain: "trust",
|
||||
Roots: []*structs.CARoot{
|
||||
{
|
||||
ID: "active",
|
||||
Name: "foo",
|
||||
SerialNumber: 42,
|
||||
SigningKeyID: "blarg",
|
||||
NotBefore: time.Unix(5000, 100),
|
||||
NotAfter: time.Unix(10000, 9009),
|
||||
RootCert: "not an actual cert",
|
||||
Active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
IssuedCert: structs.IssuedCert{
|
||||
SerialNumber: "1234",
|
||||
CertPEM: "not a cert",
|
||||
Agent: "foo",
|
||||
AgentURI: "spiffe://blarg/agent/client/dc/foo/id/foo",
|
||||
ValidAfter: time.Unix(6000, 0),
|
||||
ValidBefore: time.Unix(7000, 0),
|
||||
},
|
||||
ManualCARoots: []string{"blarg"},
|
||||
VerifyServerHostname: true,
|
||||
}
|
||||
// have to just verify that the private key was put in here but we then
|
||||
// must zero it out so that the remaining equality check will pass
|
||||
require.NotEmpty(t, actual.IssuedCert.PrivateKeyPEM)
|
||||
actual.IssuedCert.PrivateKeyPEM = ""
|
||||
require.Equal(t, expected, actual)
|
||||
|
||||
// ensure no RPC was made
|
||||
directRPC.AssertExpectations(t)
|
||||
certMon.AssertExpectations(t)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/proto"
|
||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||
"github.com/hashicorp/consul/proto/pbconfig"
|
||||
"github.com/hashicorp/consul/proto/pbconnect"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// translateAgentConfig is meant to take in a proto/pbconfig.Config type
|
||||
|
@ -158,3 +165,64 @@ func translateConfig(c *pbconfig.Config) map[string]interface{} {
|
|||
|
||||
return out
|
||||
}
|
||||
|
||||
func extractSignedResponse(resp *pbautoconf.AutoConfigResponse) (*structs.SignedResponse, error) {
|
||||
roots, err := translateCARootsToStructs(resp.CARoots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := translateIssuedCertToStructs(resp.Certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &structs.SignedResponse{
|
||||
IssuedCert: *cert,
|
||||
ConnectCARoots: *roots,
|
||||
ManualCARoots: resp.ExtraCACertificates,
|
||||
}
|
||||
|
||||
if resp.Config != nil && resp.Config.TLS != nil {
|
||||
out.VerifyServerHostname = resp.Config.TLS.VerifyServerHostname
|
||||
}
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
// translateCARootsToStructs will create a structs.IndexedCARoots object from the corresponding
|
||||
// protobuf struct. Those structs are intended to be identical so the conversion just uses
|
||||
// mapstructure to go from one to the other.
|
||||
func translateCARootsToStructs(in *pbconnect.CARoots) (*structs.IndexedCARoots, error) {
|
||||
var out structs.IndexedCARoots
|
||||
if err := mapstructureTranslateToStructs(in, &out); err != nil {
|
||||
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// translateIssuedCertToStructs will create a structs.IssuedCert object from the corresponding
|
||||
// protobuf struct. Those structs are intended to be identical so the conversion just uses
|
||||
// mapstructure to go from one to the other.
|
||||
func translateIssuedCertToStructs(in *pbconnect.IssuedCert) (*structs.IssuedCert, error) {
|
||||
var out structs.IssuedCert
|
||||
if err := mapstructureTranslateToStructs(in, &out); err != nil {
|
||||
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func mapstructureTranslateToStructs(in interface{}, out interface{}) error {
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: proto.HookPBTimestampToTime,
|
||||
Result: out,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return decoder.Decode(in)
|
||||
}
|
||||
|
|
|
@ -230,6 +230,7 @@ func (m *CertMonitor) Start(ctx context.Context) (<-chan struct{}, error) {
|
|||
exit := make(chan struct{})
|
||||
go m.run(ctx, exit)
|
||||
|
||||
m.logger.Info("certificate monitor started")
|
||||
return exit, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1297,6 +1297,10 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if rt.AutoConfig.Enabled && rt.AutoEncryptTLS {
|
||||
return fmt.Errorf("both auto_encrypt.tls and auto_config.enabled cannot be set to true.")
|
||||
}
|
||||
|
||||
if err := b.validateAutoConfig(rt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1692,10 +1692,6 @@ func (c *RuntimeConfig) ClientAddress() (unixAddr, httpAddr, httpsAddr string) {
|
|||
}
|
||||
|
||||
func (c *RuntimeConfig) ConnectCAConfiguration() (*structs.CAConfiguration, error) {
|
||||
if !c.ConnectEnabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ca := &structs.CAConfiguration{
|
||||
Provider: "consul",
|
||||
Config: map[string]interface{}{
|
||||
|
|
|
@ -3804,6 +3804,35 @@ func TestConfigFlagsAndEdgecases(t *testing.T) {
|
|||
|
||||
///////////////////////////////////
|
||||
// Auto Config related tests
|
||||
{
|
||||
desc: "auto config and auto encrypt error",
|
||||
args: []string{
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
hcl: []string{`
|
||||
auto_config {
|
||||
enabled = true
|
||||
intro_token = "blah"
|
||||
server_addresses = ["198.18.0.1"]
|
||||
}
|
||||
auto_encrypt {
|
||||
tls = true
|
||||
}
|
||||
verify_outgoing = true
|
||||
`},
|
||||
json: []string{`{
|
||||
"auto_config": {
|
||||
"enabled": true,
|
||||
"intro_token": "blah",
|
||||
"server_addresses": ["198.18.0.1"]
|
||||
},
|
||||
"auto_encrypt": {
|
||||
"tls": true
|
||||
},
|
||||
"verify_outgoing": true
|
||||
}`},
|
||||
err: "both auto_encrypt.tls and auto_config.enabled cannot be set to true.",
|
||||
},
|
||||
{
|
||||
desc: "auto config not allowed for servers",
|
||||
args: []string{
|
||||
|
@ -7480,12 +7509,6 @@ func TestConnectCAConfiguration(t *testing.T) {
|
|||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"connect-disabled": {
|
||||
config: RuntimeConfig{
|
||||
ConnectEnabled: false,
|
||||
},
|
||||
expected: nil,
|
||||
},
|
||||
"defaults": {
|
||||
config: RuntimeConfig{
|
||||
ConnectEnabled: true,
|
||||
|
|
|
@ -2,22 +2,31 @@ package consul
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/proto"
|
||||
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/lib/template"
|
||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||
config "github.com/hashicorp/consul/proto/pbconfig"
|
||||
"github.com/hashicorp/consul/proto/pbconfig"
|
||||
"github.com/hashicorp/consul/proto/pbconnect"
|
||||
"github.com/hashicorp/consul/tlsutil"
|
||||
bexpr "github.com/hashicorp/go-bexpr"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type AutoConfigOptions struct {
|
||||
NodeName string
|
||||
SegmentName string
|
||||
|
||||
CSR *x509.CertificateRequest
|
||||
SpiffeID *connect.SpiffeIDAgent
|
||||
}
|
||||
|
||||
type AutoConfigAuthorizer interface {
|
||||
|
@ -73,16 +82,35 @@ func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfig
|
|||
}
|
||||
}
|
||||
|
||||
return AutoConfigOptions{
|
||||
opts := AutoConfigOptions{
|
||||
NodeName: req.Node,
|
||||
SegmentName: req.Segment,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if req.CSR != "" {
|
||||
csr, id, err := parseAutoConfigCSR(req.CSR)
|
||||
if err != nil {
|
||||
return AutoConfigOptions{}, err
|
||||
}
|
||||
|
||||
if id.Agent != req.Node {
|
||||
return AutoConfigOptions{}, fmt.Errorf("Spiffe ID agent name (%s) of the certificate signing request is not for the correct node (%s)", id.Agent, req.Node)
|
||||
}
|
||||
|
||||
opts.CSR = csr
|
||||
opts.SpiffeID = id
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
type AutoConfigBackend interface {
|
||||
CreateACLToken(template *structs.ACLToken) (*structs.ACLToken, error)
|
||||
DatacenterJoinAddresses(segment string) ([]string, error)
|
||||
ForwardRPC(method string, info structs.RPCInfo, args, reply interface{}) (bool, error)
|
||||
|
||||
GetCARoots() (*structs.IndexedCARoots, error)
|
||||
SignCertificate(csr *x509.CertificateRequest, id connect.CertURI) (*structs.IssuedCert, error)
|
||||
}
|
||||
|
||||
// AutoConfig endpoint is used for cluster auto configuration operations
|
||||
|
@ -114,15 +142,51 @@ func NewAutoConfig(conf *Config, tlsConfigurator *tlsutil.Configurator, backend
|
|||
// made aware of its certificates are populated. This will only work if connect is enabled and
|
||||
// in some cases only if auto_encrypt is enabled on the servers. This endpoint has the option
|
||||
// to configure auto_encrypt or potentially in the future to generate the certificates inline.
|
||||
func (ac *AutoConfig) updateTLSCertificatesInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||||
conf.AutoEncrypt = &config.AutoEncrypt{TLS: ac.config.AutoEncryptAllowTLS}
|
||||
func (ac *AutoConfig) updateTLSCertificatesInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||
// nothing to be done as we cannot generate certificates
|
||||
if !ac.config.ConnectEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.CSR != nil {
|
||||
cert, err := ac.backend.SignCertificate(opts.CSR, opts.SpiffeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to sign CSR: %w", err)
|
||||
}
|
||||
|
||||
// convert to the protobuf form of the issued certificate
|
||||
pbcert, err := translateIssuedCertToProtobuf(cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Certificate = pbcert
|
||||
}
|
||||
|
||||
connectRoots, err := ac.backend.GetCARoots()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to lookup the CA roots: %w", err)
|
||||
}
|
||||
|
||||
// convert to the protobuf form of the issued certificate
|
||||
pbroots, err := translateCARootsToProtobuf(connectRoots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp.CARoots = pbroots
|
||||
|
||||
// get the non-connect CA certs from the TLS Configurator
|
||||
if ac.tlsConfigurator != nil {
|
||||
resp.ExtraCACertificates = ac.tlsConfigurator.ManualCAPems()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateACLtokensInConfig will configure all of the agents ACL settings and will populate
|
||||
// the configuration with an agent token usable for all default agent operations.
|
||||
func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||||
acl := &config.ACL{
|
||||
func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||
acl := &pbconfig.ACL{
|
||||
Enabled: ac.config.ACLsEnabled,
|
||||
PolicyTTL: ac.config.ACLPolicyTTL.String(),
|
||||
RoleTTL: ac.config.ACLRoleTTL.String(),
|
||||
|
@ -153,48 +217,48 @@ func (ac *AutoConfig) updateACLsInConfig(opts AutoConfigOptions, conf *config.Co
|
|||
return fmt.Errorf("Failed to generate an ACL token for node %q - %w", opts.NodeName, err)
|
||||
}
|
||||
|
||||
acl.Tokens = &config.ACLTokens{Agent: token.SecretID}
|
||||
acl.Tokens = &pbconfig.ACLTokens{Agent: token.SecretID}
|
||||
}
|
||||
|
||||
conf.ACL = acl
|
||||
resp.Config.ACL = acl
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateJoinAddressesInConfig determines the correct gossip endpoints that clients should
|
||||
// be connecting to for joining the cluster based on the segment given in the opts parameter.
|
||||
func (ac *AutoConfig) updateJoinAddressesInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||||
func (ac *AutoConfig) updateJoinAddressesInConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||
joinAddrs, err := ac.backend.DatacenterJoinAddresses(opts.SegmentName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conf.Gossip == nil {
|
||||
conf.Gossip = &config.Gossip{}
|
||||
if resp.Config.Gossip == nil {
|
||||
resp.Config.Gossip = &pbconfig.Gossip{}
|
||||
}
|
||||
|
||||
conf.Gossip.RetryJoinLAN = joinAddrs
|
||||
resp.Config.Gossip.RetryJoinLAN = joinAddrs
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateGossipEncryptionInConfig will populate the gossip encryption configuration settings
|
||||
func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *config.Config) error {
|
||||
func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||
// Add gossip encryption settings if there is any key loaded
|
||||
memberlistConfig := ac.config.SerfLANConfig.MemberlistConfig
|
||||
if lanKeyring := memberlistConfig.Keyring; lanKeyring != nil {
|
||||
if conf.Gossip == nil {
|
||||
conf.Gossip = &config.Gossip{}
|
||||
if resp.Config.Gossip == nil {
|
||||
resp.Config.Gossip = &pbconfig.Gossip{}
|
||||
}
|
||||
if conf.Gossip.Encryption == nil {
|
||||
conf.Gossip.Encryption = &config.GossipEncryption{}
|
||||
if resp.Config.Gossip.Encryption == nil {
|
||||
resp.Config.Gossip.Encryption = &pbconfig.GossipEncryption{}
|
||||
}
|
||||
|
||||
pk := lanKeyring.GetPrimaryKey()
|
||||
if len(pk) > 0 {
|
||||
conf.Gossip.Encryption.Key = base64.StdEncoding.EncodeToString(pk)
|
||||
resp.Config.Gossip.Encryption.Key = base64.StdEncoding.EncodeToString(pk)
|
||||
}
|
||||
|
||||
conf.Gossip.Encryption.VerifyIncoming = memberlistConfig.GossipVerifyIncoming
|
||||
conf.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
|
||||
resp.Config.Gossip.Encryption.VerifyIncoming = memberlistConfig.GossipVerifyIncoming
|
||||
resp.Config.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -202,44 +266,44 @@ func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *
|
|||
|
||||
// updateTLSSettingsInConfig will populate the TLS configuration settings but will not
|
||||
// populate leaf or ca certficiates.
|
||||
func (ac *AutoConfig) updateTLSSettingsInConfig(_ AutoConfigOptions, conf *config.Config) error {
|
||||
func (ac *AutoConfig) updateTLSSettingsInConfig(_ AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||
if ac.tlsConfigurator == nil {
|
||||
// TLS is not enabled?
|
||||
return nil
|
||||
}
|
||||
|
||||
// add in TLS configuration
|
||||
if conf.TLS == nil {
|
||||
conf.TLS = &config.TLS{}
|
||||
if resp.Config.TLS == nil {
|
||||
resp.Config.TLS = &pbconfig.TLS{}
|
||||
}
|
||||
|
||||
conf.TLS.VerifyServerHostname = ac.tlsConfigurator.VerifyServerHostname()
|
||||
resp.Config.TLS.VerifyServerHostname = ac.tlsConfigurator.VerifyServerHostname()
|
||||
base := ac.tlsConfigurator.Base()
|
||||
conf.TLS.VerifyOutgoing = base.VerifyOutgoing
|
||||
conf.TLS.MinVersion = base.TLSMinVersion
|
||||
conf.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
|
||||
resp.Config.TLS.VerifyOutgoing = base.VerifyOutgoing
|
||||
resp.Config.TLS.MinVersion = base.TLSMinVersion
|
||||
resp.Config.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
|
||||
|
||||
var err error
|
||||
conf.TLS.CipherSuites, err = tlsutil.CipherString(base.CipherSuites)
|
||||
resp.Config.TLS.CipherSuites, err = tlsutil.CipherString(base.CipherSuites)
|
||||
return err
|
||||
}
|
||||
|
||||
// baseConfig will populate the configuration with some base settings such as the
|
||||
// datacenter names, node name etc.
|
||||
func (ac *AutoConfig) baseConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||||
func (ac *AutoConfig) baseConfig(opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error {
|
||||
if opts.NodeName == "" {
|
||||
return fmt.Errorf("Cannot generate auto config response without a node name")
|
||||
}
|
||||
|
||||
conf.Datacenter = ac.config.Datacenter
|
||||
conf.PrimaryDatacenter = ac.config.PrimaryDatacenter
|
||||
conf.NodeName = opts.NodeName
|
||||
conf.SegmentName = opts.SegmentName
|
||||
resp.Config.Datacenter = ac.config.Datacenter
|
||||
resp.Config.PrimaryDatacenter = ac.config.PrimaryDatacenter
|
||||
resp.Config.NodeName = opts.NodeName
|
||||
resp.Config.SegmentName = opts.SegmentName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type autoConfigUpdater func(c *AutoConfig, opts AutoConfigOptions, conf *config.Config) error
|
||||
type autoConfigUpdater func(c *AutoConfig, opts AutoConfigOptions, resp *pbautoconf.AutoConfigResponse) error
|
||||
|
||||
var (
|
||||
// variable holding the list of config updating functions to execute when generating
|
||||
|
@ -290,15 +354,71 @@ func (ac *AutoConfig) InitialConfiguration(req *pbautoconf.AutoConfigRequest, re
|
|||
return err
|
||||
}
|
||||
|
||||
conf := &config.Config{}
|
||||
resp.Config = &pbconfig.Config{}
|
||||
|
||||
// update all the configurations
|
||||
for _, configFn := range autoConfigUpdaters {
|
||||
if err := configFn(ac, opts, conf); err != nil {
|
||||
if err := configFn(ac, opts, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resp.Config = conf
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAutoConfigCSR(csr string) (*x509.CertificateRequest, *connect.SpiffeIDAgent, error) {
|
||||
// Parse the CSR string into the x509 CertificateRequest struct
|
||||
x509CSR, err := connect.ParseCSR(csr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to parse CSR: %w", err)
|
||||
}
|
||||
|
||||
// ensure that a URI SAN is present
|
||||
if len(x509CSR.URIs) < 1 {
|
||||
return nil, nil, fmt.Errorf("CSR didn't include any URI SANs")
|
||||
}
|
||||
|
||||
// Parse the SPIFFE ID
|
||||
spiffeID, err := connect.ParseCertURI(x509CSR.URIs[0])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to parse the SPIFFE URI: %w", err)
|
||||
}
|
||||
|
||||
agentID, isAgent := spiffeID.(*connect.SpiffeIDAgent)
|
||||
if !isAgent {
|
||||
return nil, nil, fmt.Errorf("SPIFFE ID is not an Agent ID")
|
||||
}
|
||||
|
||||
return x509CSR, agentID, nil
|
||||
}
|
||||
|
||||
func translateCARootsToProtobuf(in *structs.IndexedCARoots) (*pbconnect.CARoots, error) {
|
||||
var out pbconnect.CARoots
|
||||
if err := mapstructureTranslateToProtobuf(in, &out); err != nil {
|
||||
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func translateIssuedCertToProtobuf(in *structs.IssuedCert) (*pbconnect.IssuedCert, error) {
|
||||
var out pbconnect.IssuedCert
|
||||
if err := mapstructureTranslateToProtobuf(in, &out); err != nil {
|
||||
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func mapstructureTranslateToProtobuf(in interface{}, out interface{}) error {
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: proto.HookTimeToPBTimestamp,
|
||||
Result: out,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return decoder.Decode(in)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||
|
@ -48,6 +50,18 @@ func (m *mockAutoConfigBackend) ForwardRPC(method string, info structs.RPCInfo,
|
|||
return ret.Bool(0), ret.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockAutoConfigBackend) GetCARoots() (*structs.IndexedCARoots, error) {
|
||||
ret := m.Called()
|
||||
roots, _ := ret.Get(0).(*structs.IndexedCARoots)
|
||||
return roots, ret.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockAutoConfigBackend) SignCertificate(csr *x509.CertificateRequest, id connect.CertURI) (*structs.IssuedCert, error) {
|
||||
ret := m.Called(csr, id)
|
||||
cert, _ := ret.Get(0).(*structs.IssuedCert)
|
||||
return cert, ret.Error(1)
|
||||
}
|
||||
|
||||
func testJWTStandardClaims() jwt.Claims {
|
||||
now := time.Now()
|
||||
|
||||
|
@ -79,13 +93,6 @@ func signJWTWithStandardClaims(t *testing.T, privKey string, claims interface{})
|
|||
// * Each of the individual config generation functions. These can be unit tested separately and should NOT
|
||||
// require running test servers
|
||||
func TestAutoConfigInitialConfiguration(t *testing.T) {
|
||||
type testCase struct {
|
||||
request pbautoconf.AutoConfigRequest
|
||||
expected pbautoconf.AutoConfigResponse
|
||||
patchResponse func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse)
|
||||
err string
|
||||
}
|
||||
|
||||
gossipKey := make([]byte, 32)
|
||||
// this is not cryptographic randomness and is not secure but for the sake of this test its all we need.
|
||||
n, err := rand.Read(gossipKey)
|
||||
|
@ -105,85 +112,21 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
|
|||
_, altpriv, err := oidcauthtest.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := map[string]testCase{
|
||||
"wrong-datacenter": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Datacenter: "no-such-dc",
|
||||
},
|
||||
err: `invalid datacenter "no-such-dc" - agent auto configuration cannot target a remote datacenter`,
|
||||
},
|
||||
"unverifiable": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Node: "test-node",
|
||||
// this is signed using an incorrect private key
|
||||
JWT: signJWTWithStandardClaims(t, altpriv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||
},
|
||||
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
|
||||
},
|
||||
"claim-assertion-failed": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Node: "test-node",
|
||||
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"wrong_claim": "test-node"}),
|
||||
},
|
||||
err: "Permission denied: Failed JWT claim assertion",
|
||||
},
|
||||
"good": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Node: "test-node",
|
||||
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||
},
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
Datacenter: "dc1",
|
||||
PrimaryDatacenter: "dc1",
|
||||
NodeName: "test-node",
|
||||
AutoEncrypt: &pbconfig.AutoEncrypt{
|
||||
TLS: true,
|
||||
},
|
||||
ACL: &pbconfig.ACL{
|
||||
Enabled: true,
|
||||
PolicyTTL: "30s",
|
||||
TokenTTL: "30s",
|
||||
RoleTTL: "30s",
|
||||
DisabledTTL: "0s",
|
||||
DownPolicy: "extend-cache",
|
||||
DefaultPolicy: "deny",
|
||||
Tokens: &pbconfig.ACLTokens{
|
||||
Agent: "patched-secret",
|
||||
},
|
||||
},
|
||||
Gossip: &pbconfig.Gossip{
|
||||
Encryption: &pbconfig.GossipEncryption{
|
||||
Key: gossipKeyEncoded,
|
||||
VerifyIncoming: true,
|
||||
VerifyOutgoing: true,
|
||||
},
|
||||
},
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: true,
|
||||
MinVersion: "tls12",
|
||||
PreferServerCipherSuites: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
patchResponse: func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse) {
|
||||
// we are expecting an ACL token but cannot check anything for equality
|
||||
// so here we check that it was set and overwrite it
|
||||
require.NotNil(t, resp.Config)
|
||||
require.NotNil(t, resp.Config.ACL)
|
||||
require.NotNil(t, resp.Config.ACL.Tokens)
|
||||
require.NotEmpty(t, resp.Config.ACL.Tokens.Agent)
|
||||
resp.Config.ACL.Tokens.Agent = "patched-secret"
|
||||
|
||||
// we don't know the expected join address until we start up the test server
|
||||
joinAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: srv.config.SerfLANConfig.MemberlistConfig.AdvertisePort}
|
||||
require.NotNil(t, resp.Config.Gossip)
|
||||
require.Equal(t, []string{joinAddr.String()}, resp.Config.Gossip.RetryJoinLAN)
|
||||
resp.Config.Gossip.RetryJoinLAN = nil
|
||||
},
|
||||
},
|
||||
// this CSR is what gets sent in the request
|
||||
csrID := connect.SpiffeIDAgent{
|
||||
Host: "dummy.trustdomain",
|
||||
Agent: "test-node",
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
csr, _ := connect.TestCSR(t, &csrID)
|
||||
|
||||
altCSRID := connect.SpiffeIDAgent{
|
||||
Host: "dummy.trustdomain",
|
||||
Agent: "alt",
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
|
||||
altCSR, _ := connect.TestCSR(t, &altCSRID)
|
||||
|
||||
_, s, _ := testACLServerWithConfig(t, func(c *Config) {
|
||||
c.Domain = "consul"
|
||||
|
@ -248,6 +191,126 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
|
|||
|
||||
waitForLeaderEstablishment(t, s)
|
||||
|
||||
roots, err := s.GetCARoots()
|
||||
require.NoError(t, err)
|
||||
|
||||
pbroots, err := translateCARootsToProtobuf(roots)
|
||||
require.NoError(t, err)
|
||||
|
||||
joinAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: s.config.SerfLANConfig.MemberlistConfig.AdvertisePort}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Common test setup is now complete
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
type testCase struct {
|
||||
request pbautoconf.AutoConfigRequest
|
||||
expected pbautoconf.AutoConfigResponse
|
||||
patchResponse func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse)
|
||||
err string
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"wrong-datacenter": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Datacenter: "no-such-dc",
|
||||
},
|
||||
err: `invalid datacenter "no-such-dc" - agent auto configuration cannot target a remote datacenter`,
|
||||
},
|
||||
"unverifiable": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Node: "test-node",
|
||||
// this is signed using an incorrect private key
|
||||
JWT: signJWTWithStandardClaims(t, altpriv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||
},
|
||||
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
|
||||
},
|
||||
"claim-assertion-failed": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Node: "test-node",
|
||||
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"wrong_claim": "test-node"}),
|
||||
},
|
||||
err: "Permission denied: Failed JWT claim assertion",
|
||||
},
|
||||
"bad-csr-id": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Node: "test-node",
|
||||
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||
CSR: altCSR,
|
||||
},
|
||||
err: "Spiffe ID agent name (alt) of the certificate signing request is not for the correct node (test-node)",
|
||||
},
|
||||
"good": {
|
||||
request: pbautoconf.AutoConfigRequest{
|
||||
Node: "test-node",
|
||||
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||
CSR: csr,
|
||||
},
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
CARoots: pbroots,
|
||||
ExtraCACertificates: []string{cacert},
|
||||
Config: &pbconfig.Config{
|
||||
Datacenter: "dc1",
|
||||
PrimaryDatacenter: "dc1",
|
||||
NodeName: "test-node",
|
||||
ACL: &pbconfig.ACL{
|
||||
Enabled: true,
|
||||
PolicyTTL: "30s",
|
||||
TokenTTL: "30s",
|
||||
RoleTTL: "30s",
|
||||
DisabledTTL: "0s",
|
||||
DownPolicy: "extend-cache",
|
||||
DefaultPolicy: "deny",
|
||||
Tokens: &pbconfig.ACLTokens{
|
||||
Agent: "patched-secret",
|
||||
},
|
||||
},
|
||||
Gossip: &pbconfig.Gossip{
|
||||
Encryption: &pbconfig.GossipEncryption{
|
||||
Key: gossipKeyEncoded,
|
||||
VerifyIncoming: true,
|
||||
VerifyOutgoing: true,
|
||||
},
|
||||
RetryJoinLAN: []string{joinAddr.String()},
|
||||
},
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: true,
|
||||
MinVersion: "tls12",
|
||||
PreferServerCipherSuites: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
patchResponse: func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse) {
|
||||
// we are expecting an ACL token but cannot check anything for equality
|
||||
// so here we check that it was set and overwrite it
|
||||
require.NotNil(t, resp.Config)
|
||||
require.NotNil(t, resp.Config.ACL)
|
||||
require.NotNil(t, resp.Config.ACL.Tokens)
|
||||
require.NotEmpty(t, resp.Config.ACL.Tokens.Agent)
|
||||
resp.Config.ACL.Tokens.Agent = "patched-secret"
|
||||
|
||||
require.NotNil(t, resp.Certificate)
|
||||
require.NotEmpty(t, resp.Certificate.SerialNumber)
|
||||
require.NotEmpty(t, resp.Certificate.CertPEM)
|
||||
require.Empty(t, resp.Certificate.Service)
|
||||
require.Empty(t, resp.Certificate.ServiceURI)
|
||||
require.Equal(t, "test-node", resp.Certificate.Agent)
|
||||
|
||||
expectedID := connect.SpiffeIDAgent{
|
||||
Host: roots.TrustDomain,
|
||||
Agent: "test-node",
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
|
||||
require.Equal(t, expectedID.URI().String(), resp.Certificate.AgentURI)
|
||||
|
||||
// nil this out so we don't check it for equality
|
||||
resp.Certificate = nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for testName, tcase := range cases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
var reply pbautoconf.AutoConfigResponse
|
||||
|
@ -269,7 +332,7 @@ func TestAutoConfig_baseConfig(t *testing.T) {
|
|||
type testCase struct {
|
||||
serverConfig Config
|
||||
opts AutoConfigOptions
|
||||
expected pbconfig.Config
|
||||
expected pbautoconf.AutoConfigResponse
|
||||
err string
|
||||
}
|
||||
|
||||
|
@ -283,11 +346,13 @@ func TestAutoConfig_baseConfig(t *testing.T) {
|
|||
NodeName: "lBdc0lsH",
|
||||
SegmentName: "HZiwlWpi",
|
||||
},
|
||||
expected: pbconfig.Config{
|
||||
Datacenter: "oSWzfhnU",
|
||||
PrimaryDatacenter: "53XO9mx4",
|
||||
NodeName: "lBdc0lsH",
|
||||
SegmentName: "HZiwlWpi",
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
Datacenter: "oSWzfhnU",
|
||||
PrimaryDatacenter: "53XO9mx4",
|
||||
NodeName: "lBdc0lsH",
|
||||
SegmentName: "HZiwlWpi",
|
||||
},
|
||||
},
|
||||
},
|
||||
"no-node-name": {
|
||||
|
@ -305,7 +370,7 @@ func TestAutoConfig_baseConfig(t *testing.T) {
|
|||
config: &tcase.serverConfig,
|
||||
}
|
||||
|
||||
var actual pbconfig.Config
|
||||
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||
err := ac.baseConfig(tcase.opts, &actual)
|
||||
if tcase.err == "" {
|
||||
require.NoError(t, err)
|
||||
|
@ -317,6 +382,13 @@ func TestAutoConfig_baseConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func parseCiphers(t *testing.T, cipherStr string) []uint16 {
|
||||
t.Helper()
|
||||
ciphers, err := tlsutil.ParseCiphers(cipherStr)
|
||||
require.NoError(t, err)
|
||||
return ciphers
|
||||
}
|
||||
|
||||
func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
||||
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
|
||||
require.NoError(t, err)
|
||||
|
@ -328,16 +400,9 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
|||
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
parseCiphers := func(t *testing.T, cipherStr string) []uint16 {
|
||||
t.Helper()
|
||||
ciphers, err := tlsutil.ParseCiphers(cipherStr)
|
||||
require.NoError(t, err)
|
||||
return ciphers
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
tlsConfig tlsutil.Config
|
||||
expected pbconfig.Config
|
||||
expected pbautoconf.AutoConfigResponse
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
|
@ -350,13 +415,15 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
|||
CAFile: cafile,
|
||||
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
|
||||
},
|
||||
expected: pbconfig.Config{
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: true,
|
||||
MinVersion: "tls12",
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: true,
|
||||
MinVersion: "tls12",
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -369,13 +436,15 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
|||
CAFile: cafile,
|
||||
CipherSuites: parseCiphers(t, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
|
||||
},
|
||||
expected: pbconfig.Config{
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: false,
|
||||
MinVersion: "tls10",
|
||||
PreferServerCipherSuites: false,
|
||||
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
TLS: &pbconfig.TLS{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: false,
|
||||
MinVersion: "tls10",
|
||||
PreferServerCipherSuites: false,
|
||||
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -391,7 +460,7 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
|||
tlsConfigurator: configurator,
|
||||
}
|
||||
|
||||
var actual pbconfig.Config
|
||||
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||
err = ac.updateTLSSettingsInConfig(AutoConfigOptions{}, &actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tcase.expected, actual)
|
||||
|
@ -402,7 +471,7 @@ func TestAutoConfig_updateTLSSettingsInConfig(t *testing.T) {
|
|||
func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
||||
type testCase struct {
|
||||
conf memberlist.Config
|
||||
expected pbconfig.Config
|
||||
expected pbautoconf.AutoConfigResponse
|
||||
}
|
||||
|
||||
gossipKey := make([]byte, 32)
|
||||
|
@ -422,12 +491,14 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
|||
GossipVerifyIncoming: true,
|
||||
GossipVerifyOutgoing: true,
|
||||
},
|
||||
expected: pbconfig.Config{
|
||||
Gossip: &pbconfig.Gossip{
|
||||
Encryption: &pbconfig.GossipEncryption{
|
||||
Key: gossipKeyEncoded,
|
||||
VerifyIncoming: true,
|
||||
VerifyOutgoing: true,
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
Gossip: &pbconfig.Gossip{
|
||||
Encryption: &pbconfig.GossipEncryption{
|
||||
Key: gossipKeyEncoded,
|
||||
VerifyIncoming: true,
|
||||
VerifyOutgoing: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -438,12 +509,14 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
|||
GossipVerifyIncoming: false,
|
||||
GossipVerifyOutgoing: false,
|
||||
},
|
||||
expected: pbconfig.Config{
|
||||
Gossip: &pbconfig.Gossip{
|
||||
Encryption: &pbconfig.GossipEncryption{
|
||||
Key: gossipKeyEncoded,
|
||||
VerifyIncoming: false,
|
||||
VerifyOutgoing: false,
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
Gossip: &pbconfig.Gossip{
|
||||
Encryption: &pbconfig.GossipEncryption{
|
||||
Key: gossipKeyEncoded,
|
||||
VerifyIncoming: false,
|
||||
VerifyOutgoing: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -451,6 +524,9 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
|||
"encryption-disabled": {
|
||||
// zero values all around - if no keyring is configured then the gossip
|
||||
// encryption settings should not be set.
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -463,7 +539,7 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
|||
config: cfg,
|
||||
}
|
||||
|
||||
var actual pbconfig.Config
|
||||
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||
err := ac.updateGossipEncryptionInConfig(AutoConfigOptions{}, &actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tcase.expected, actual)
|
||||
|
@ -472,40 +548,149 @@ func TestAutoConfig_updateGossipEncryptionInConfig(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
|
||||
now := time.Now()
|
||||
later := now.Add(time.Hour)
|
||||
|
||||
// Generate a Test CA
|
||||
ca := connect.TestCA(t, nil)
|
||||
|
||||
// roots will be returned by the mock backend
|
||||
roots := structs.IndexedCARoots{
|
||||
ActiveRootID: ca.ID,
|
||||
TrustDomain: connect.TestClusterID + ".consul",
|
||||
Roots: []*structs.CARoot{
|
||||
ca,
|
||||
},
|
||||
}
|
||||
|
||||
// this CSR is what gets put into the opts for the
|
||||
// function to look at an process
|
||||
csrID := connect.SpiffeIDAgent{
|
||||
Host: roots.TrustDomain,
|
||||
Agent: "test",
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
csrStr, _ := connect.TestCSR(t, &csrID)
|
||||
|
||||
csr, err := connect.ParseCSR(csrStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fake certificate response for the backend
|
||||
fakeCert := structs.IssuedCert{
|
||||
SerialNumber: "1",
|
||||
CertPEM: "not-currently-decoded",
|
||||
ValidAfter: now,
|
||||
ValidBefore: later,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
RaftIndex: structs.RaftIndex{
|
||||
ModifyIndex: 10,
|
||||
CreateIndex: 10,
|
||||
},
|
||||
}
|
||||
|
||||
// translate the fake cert to the protobuf equivalent
|
||||
// for embedding in expected results
|
||||
pbcert, err := translateIssuedCertToProtobuf(&fakeCert)
|
||||
require.NoError(t, err)
|
||||
|
||||
// generate a CA certificate to use for specifying non-Connect
|
||||
// certificates which come back differently in the response
|
||||
_, _, cacert, err := testTLSCertificates("server.dc1.consul")
|
||||
require.NoError(t, err)
|
||||
|
||||
// write out that ca cert to disk - it is unfortunate that
|
||||
// this is necessary but creation of the tlsutil.Configurator
|
||||
// will error if it cannot load the CA certificate from disk
|
||||
dir := testutil.TempDir(t, "auto-config-tls-certificate")
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
|
||||
cafile := path.Join(dir, "cacert.pem")
|
||||
err = ioutil.WriteFile(cafile, []byte(cacert), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// translate the roots response to protobuf to be embedded
|
||||
// into the expected results
|
||||
pbroots, err := translateCARootsToProtobuf(&roots)
|
||||
require.NoError(t, err)
|
||||
|
||||
type testCase struct {
|
||||
serverConfig Config
|
||||
expected pbconfig.Config
|
||||
tlsConfig tlsutil.Config
|
||||
|
||||
opts AutoConfigOptions
|
||||
expected pbautoconf.AutoConfigResponse
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"auto_encrypt-enabled": {
|
||||
"no-csr": {
|
||||
serverConfig: Config{
|
||||
ConnectEnabled: true,
|
||||
AutoEncryptAllowTLS: true,
|
||||
ConnectEnabled: true,
|
||||
},
|
||||
expected: pbconfig.Config{
|
||||
AutoEncrypt: &pbconfig.AutoEncrypt{TLS: true},
|
||||
tlsConfig: tlsutil.Config{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: true,
|
||||
TLSMinVersion: "tls12",
|
||||
PreferServerCipherSuites: true,
|
||||
CAFile: cafile,
|
||||
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
|
||||
},
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
CARoots: pbroots,
|
||||
ExtraCACertificates: []string{cacert},
|
||||
Config: &pbconfig.Config{},
|
||||
},
|
||||
},
|
||||
"auto_encrypt-disabled": {
|
||||
"signed-certificate": {
|
||||
serverConfig: Config{
|
||||
ConnectEnabled: true,
|
||||
AutoEncryptAllowTLS: false,
|
||||
ConnectEnabled: true,
|
||||
},
|
||||
expected: pbconfig.Config{
|
||||
AutoEncrypt: &pbconfig.AutoEncrypt{TLS: false},
|
||||
tlsConfig: tlsutil.Config{
|
||||
VerifyOutgoing: true,
|
||||
VerifyServerHostname: true,
|
||||
TLSMinVersion: "tls12",
|
||||
PreferServerCipherSuites: true,
|
||||
CAFile: cafile,
|
||||
CipherSuites: parseCiphers(t, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"),
|
||||
},
|
||||
opts: AutoConfigOptions{
|
||||
NodeName: "test",
|
||||
CSR: csr,
|
||||
SpiffeID: &csrID,
|
||||
},
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{},
|
||||
CARoots: pbroots,
|
||||
ExtraCACertificates: []string{cacert},
|
||||
Certificate: pbcert,
|
||||
},
|
||||
},
|
||||
"connect-disabled": {
|
||||
serverConfig: Config{
|
||||
ConnectEnabled: false,
|
||||
},
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tcase := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
backend := &mockAutoConfigBackend{}
|
||||
backend.On("GetCARoots").Return(&roots, nil)
|
||||
backend.On("SignCertificate", tcase.opts.CSR, tcase.opts.SpiffeID).Return(&fakeCert, nil)
|
||||
|
||||
tlsConfigurator, err := tlsutil.NewConfigurator(tcase.tlsConfig, testutil.Logger(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
ac := AutoConfig{
|
||||
config: &tcase.serverConfig,
|
||||
config: &tcase.serverConfig,
|
||||
tlsConfigurator: tlsConfigurator,
|
||||
backend: backend,
|
||||
}
|
||||
|
||||
var actual pbconfig.Config
|
||||
err := ac.updateTLSCertificatesInConfig(AutoConfigOptions{}, &actual)
|
||||
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||
err = ac.updateTLSCertificatesInConfig(tcase.opts, &actual)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tcase.expected, actual)
|
||||
})
|
||||
|
@ -515,7 +700,7 @@ func TestAutoConfig_updateTLSCertificatesInConfig(t *testing.T) {
|
|||
func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
||||
type testCase struct {
|
||||
config Config
|
||||
expected pbconfig.Config
|
||||
expected pbautoconf.AutoConfigResponse
|
||||
expectACLToken bool
|
||||
err error
|
||||
}
|
||||
|
@ -542,18 +727,20 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
|||
ACLEnableKeyListPolicy: true,
|
||||
},
|
||||
expectACLToken: true,
|
||||
expected: pbconfig.Config{
|
||||
ACL: &pbconfig.ACL{
|
||||
Enabled: true,
|
||||
PolicyTTL: "7s",
|
||||
RoleTTL: "10s",
|
||||
TokenTTL: "12s",
|
||||
DisabledTTL: "31s",
|
||||
DownPolicy: "deny",
|
||||
DefaultPolicy: "allow",
|
||||
EnableKeyListPolicy: true,
|
||||
Tokens: &pbconfig.ACLTokens{
|
||||
Agent: tokenSecret,
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
ACL: &pbconfig.ACL{
|
||||
Enabled: true,
|
||||
PolicyTTL: "7s",
|
||||
RoleTTL: "10s",
|
||||
TokenTTL: "12s",
|
||||
DisabledTTL: "31s",
|
||||
DownPolicy: "deny",
|
||||
DefaultPolicy: "allow",
|
||||
EnableKeyListPolicy: true,
|
||||
Tokens: &pbconfig.ACLTokens{
|
||||
Agent: tokenSecret,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -572,16 +759,18 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
|||
ACLEnableKeyListPolicy: true,
|
||||
},
|
||||
expectACLToken: false,
|
||||
expected: pbconfig.Config{
|
||||
ACL: &pbconfig.ACL{
|
||||
Enabled: false,
|
||||
PolicyTTL: "7s",
|
||||
RoleTTL: "10s",
|
||||
TokenTTL: "12s",
|
||||
DisabledTTL: "31s",
|
||||
DownPolicy: "deny",
|
||||
DefaultPolicy: "allow",
|
||||
EnableKeyListPolicy: true,
|
||||
expected: pbautoconf.AutoConfigResponse{
|
||||
Config: &pbconfig.Config{
|
||||
ACL: &pbconfig.ACL{
|
||||
Enabled: false,
|
||||
PolicyTTL: "7s",
|
||||
RoleTTL: "10s",
|
||||
TokenTTL: "12s",
|
||||
DisabledTTL: "31s",
|
||||
DownPolicy: "deny",
|
||||
DefaultPolicy: "allow",
|
||||
EnableKeyListPolicy: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -630,7 +819,7 @@ func TestAutoConfig_updateACLsInConfig(t *testing.T) {
|
|||
|
||||
ac := AutoConfig{config: &tcase.config, backend: backend}
|
||||
|
||||
var actual pbconfig.Config
|
||||
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||
err := ac.updateACLsInConfig(AutoConfigOptions{NodeName: "something"}, &actual)
|
||||
if tcase.err != nil {
|
||||
testutil.RequireErrorContains(t, err, tcase.err.Error())
|
||||
|
@ -651,12 +840,12 @@ func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
|
|||
|
||||
ac := AutoConfig{backend: backend}
|
||||
|
||||
var actual pbconfig.Config
|
||||
actual := pbautoconf.AutoConfigResponse{Config: &pbconfig.Config{}}
|
||||
err := ac.updateJoinAddressesInConfig(AutoConfigOptions{}, &actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, actual.Gossip)
|
||||
require.ElementsMatch(t, addrs, actual.Gossip.RetryJoinLAN)
|
||||
require.NotNil(t, actual.Config.Gossip)
|
||||
require.ElementsMatch(t, addrs, actual.Config.Gossip.RetryJoinLAN)
|
||||
|
||||
backend.AssertExpectations(t)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -523,8 +524,8 @@ func NewServerWithOptions(config *Config, options ...ConsulOption) (*Server, err
|
|||
return nil, fmt.Errorf("Failed to start Raft: %v", err)
|
||||
}
|
||||
|
||||
if s.config.ConnectEnabled && s.config.AutoEncryptAllowTLS {
|
||||
go s.trackAutoEncryptCARoots()
|
||||
if s.config.ConnectEnabled && (s.config.AutoEncryptAllowTLS || s.config.AutoConfigAuthzEnabled) {
|
||||
go s.connectCARootsMonitor(&lib.StopChannelContext{StopCh: s.shutdownCh})
|
||||
}
|
||||
|
||||
if s.gatewayLocator != nil {
|
||||
|
@ -632,18 +633,11 @@ func NewServerWithOptions(config *Config, options ...ConsulOption) (*Server, err
|
|||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) trackAutoEncryptCARoots() {
|
||||
func (s *Server) connectCARootsMonitor(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-s.shutdownCh:
|
||||
s.logger.Debug("shutting down trackAutoEncryptCARoots because shutdown")
|
||||
return
|
||||
default:
|
||||
}
|
||||
ws := memdb.NewWatchSet()
|
||||
state := s.fsm.State()
|
||||
ws.Add(state.AbandonCh())
|
||||
ws.Add(s.shutdownCh)
|
||||
_, cas, err := state.CARoots(ws)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to watch AutoEncrypt CARoot", "error", err)
|
||||
|
@ -656,7 +650,11 @@ func (s *Server) trackAutoEncryptCARoots() {
|
|||
if err := s.tlsConfigurator.UpdateAutoEncryptCA(caPems); err != nil {
|
||||
s.logger.Error("Failed to update AutoEncrypt CAPems", "error", err)
|
||||
}
|
||||
ws.Watch(nil)
|
||||
|
||||
if err := ws.WatchCtx(ctx); err == context.Canceled {
|
||||
s.logger.Info("shutting down Connect CA roots monitor")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
pbconfig "github.com/hashicorp/consul/proto/pbconfig"
|
||||
pbconnect "github.com/hashicorp/consul/proto/pbconnect"
|
||||
io "io"
|
||||
math "math"
|
||||
)
|
||||
|
@ -39,7 +40,10 @@ type AutoConfigRequest struct {
|
|||
JWT string `protobuf:"bytes,5,opt,name=JWT,proto3" json:"JWT,omitempty"`
|
||||
// ConsulToken is a Consul ACL token that the agent requesting the
|
||||
// configuration already has.
|
||||
ConsulToken string `protobuf:"bytes,6,opt,name=ConsulToken,proto3" json:"ConsulToken,omitempty"`
|
||||
ConsulToken string `protobuf:"bytes,6,opt,name=ConsulToken,proto3" json:"ConsulToken,omitempty"`
|
||||
// CSR is a certificate signing request to be used when generating the
|
||||
// agents TLS certificate
|
||||
CSR string `protobuf:"bytes,7,opt,name=CSR,proto3" json:"CSR,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
|
@ -113,12 +117,27 @@ func (m *AutoConfigRequest) GetConsulToken() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (m *AutoConfigRequest) GetCSR() string {
|
||||
if m != nil {
|
||||
return m.CSR
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AutoConfigResponse is the data structure sent in response to a AutoConfig.InitialConfiguration request
|
||||
type AutoConfigResponse struct {
|
||||
Config *pbconfig.Config `protobuf:"bytes,1,opt,name=Config,proto3" json:"Config,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
// Config is the partial Consul configuration to inject into the agents own configuration
|
||||
Config *pbconfig.Config `protobuf:"bytes,1,opt,name=Config,proto3" json:"Config,omitempty"`
|
||||
// CARoots is the current list of Connect CA Roots
|
||||
CARoots *pbconnect.CARoots `protobuf:"bytes,2,opt,name=CARoots,proto3" json:"CARoots,omitempty"`
|
||||
// Certificate is the TLS certificate issued for the agent
|
||||
Certificate *pbconnect.IssuedCert `protobuf:"bytes,3,opt,name=Certificate,proto3" json:"Certificate,omitempty"`
|
||||
// ExtraCACertificates holds non-Connect certificates that may be necessary
|
||||
// to verify TLS connections with the Consul servers
|
||||
ExtraCACertificates []string `protobuf:"bytes,4,rep,name=ExtraCACertificates,proto3" json:"ExtraCACertificates,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *AutoConfigResponse) Reset() { *m = AutoConfigResponse{} }
|
||||
|
@ -161,31 +180,60 @@ func (m *AutoConfigResponse) GetConfig() *pbconfig.Config {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *AutoConfigResponse) GetCARoots() *pbconnect.CARoots {
|
||||
if m != nil {
|
||||
return m.CARoots
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AutoConfigResponse) GetCertificate() *pbconnect.IssuedCert {
|
||||
if m != nil {
|
||||
return m.Certificate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AutoConfigResponse) GetExtraCACertificates() []string {
|
||||
if m != nil {
|
||||
return m.ExtraCACertificates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*AutoConfigRequest)(nil), "autoconf.AutoConfigRequest")
|
||||
proto.RegisterType((*AutoConfigResponse)(nil), "autoconf.AutoConfigResponse")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("proto/pbautoconf/auto_config.proto", fileDescriptor_ccc5af992e5daf69) }
|
||||
func init() {
|
||||
proto.RegisterFile("proto/pbautoconf/auto_config.proto", fileDescriptor_ccc5af992e5daf69)
|
||||
}
|
||||
|
||||
var fileDescriptor_ccc5af992e5daf69 = []byte{
|
||||
// 256 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x2a, 0x28, 0xca, 0x2f,
|
||||
0xc9, 0xd7, 0x2f, 0x48, 0x4a, 0x2c, 0x2d, 0xc9, 0x4f, 0xce, 0xcf, 0x4b, 0xd3, 0x07, 0x31, 0xe2,
|
||||
0x41, 0xac, 0xcc, 0x74, 0x3d, 0xb0, 0xa4, 0x10, 0x07, 0x4c, 0x4e, 0x4a, 0x1a, 0xa6, 0x1a, 0x22,
|
||||
0xaf, 0x8f, 0xac, 0x4c, 0x69, 0x2a, 0x23, 0x97, 0xa0, 0x63, 0x69, 0x49, 0xbe, 0x33, 0x58, 0x30,
|
||||
0x28, 0xb5, 0xb0, 0x34, 0xb5, 0xb8, 0x44, 0x48, 0x8e, 0x8b, 0xcb, 0x25, 0xb1, 0x24, 0x31, 0x39,
|
||||
0x35, 0xaf, 0x24, 0xb5, 0x48, 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x08, 0x49, 0x44, 0x48, 0x88,
|
||||
0x8b, 0xc5, 0x2f, 0x3f, 0x25, 0x55, 0x82, 0x09, 0x2c, 0x03, 0x66, 0x0b, 0x49, 0x70, 0xb1, 0x07,
|
||||
0xa7, 0xa6, 0xe7, 0xa6, 0xe6, 0x95, 0x48, 0xb0, 0x80, 0x85, 0x61, 0x5c, 0x21, 0x01, 0x2e, 0x66,
|
||||
0xaf, 0xf0, 0x10, 0x09, 0x56, 0xb0, 0x28, 0x88, 0x29, 0xa4, 0xc0, 0xc5, 0xed, 0x9c, 0x9f, 0x57,
|
||||
0x5c, 0x9a, 0x13, 0x92, 0x9f, 0x9d, 0x9a, 0x27, 0xc1, 0x06, 0x96, 0x41, 0x16, 0x52, 0xb2, 0xe1,
|
||||
0x12, 0x42, 0x76, 0x56, 0x71, 0x41, 0x7e, 0x5e, 0x71, 0xaa, 0x90, 0x1a, 0x17, 0x1b, 0x44, 0x04,
|
||||
0xec, 0x26, 0x6e, 0x23, 0x3e, 0x3d, 0xa8, 0x67, 0xa0, 0xea, 0xa0, 0xb2, 0x4e, 0x76, 0x27, 0x1e,
|
||||
0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x8c, 0xc7, 0x72, 0x0c, 0x51,
|
||||
0x3a, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xfa, 0x19, 0x89, 0xc5, 0x19,
|
||||
0x99, 0xc9, 0xf9, 0x45, 0x05, 0xa0, 0xa0, 0x28, 0x2e, 0xcd, 0xd1, 0x47, 0x0f, 0xce, 0x24, 0x36,
|
||||
0xb0, 0x88, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x46, 0x7e, 0xde, 0xed, 0x69, 0x01, 0x00, 0x00,
|
||||
// 347 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x92, 0xc1, 0x4a, 0xc3, 0x30,
|
||||
0x18, 0xc7, 0x8d, 0x9b, 0x9b, 0x4b, 0x41, 0x66, 0x76, 0x09, 0x13, 0xcb, 0xd8, 0x41, 0x86, 0x48,
|
||||
0x2b, 0x13, 0xaf, 0xc2, 0x8c, 0x1e, 0xf4, 0xe0, 0x21, 0x1b, 0x08, 0x5e, 0xa4, 0xeb, 0xb2, 0xad,
|
||||
0xb8, 0x25, 0xb5, 0xf9, 0x02, 0x3e, 0x8a, 0x6f, 0xe0, 0xab, 0x78, 0xd4, 0x37, 0x90, 0xf9, 0x22,
|
||||
0xd2, 0xb4, 0x95, 0x20, 0x9e, 0xfa, 0xef, 0xff, 0xf7, 0xfb, 0xe0, 0x6b, 0x13, 0xdc, 0x4f, 0x33,
|
||||
0x05, 0x2a, 0x4c, 0xa7, 0x91, 0x01, 0x15, 0x2b, 0x39, 0x0f, 0xf3, 0xf0, 0x98, 0xa7, 0x64, 0x11,
|
||||
0x58, 0x48, 0x76, 0x2b, 0xd6, 0x3d, 0xa8, 0xec, 0x82, 0x87, 0xae, 0xd6, 0x3d, 0x74, 0xa0, 0x14,
|
||||
0x31, 0x84, 0xe5, 0xb3, 0xc0, 0xfd, 0x37, 0x84, 0xf7, 0x47, 0x06, 0x14, 0xb3, 0x33, 0x5c, 0x3c,
|
||||
0x1b, 0xa1, 0x81, 0xf8, 0x18, 0x5f, 0x45, 0x10, 0xc5, 0x42, 0x82, 0xc8, 0x28, 0xea, 0xa1, 0x41,
|
||||
0x8b, 0x3b, 0x0d, 0x21, 0xb8, 0x7e, 0xa7, 0x66, 0x82, 0x6e, 0x5b, 0x62, 0x33, 0xa1, 0xb8, 0x39,
|
||||
0x16, 0x8b, 0xb5, 0x90, 0x40, 0xeb, 0xb6, 0xae, 0x5e, 0x49, 0x1b, 0xd7, 0x6e, 0xef, 0x27, 0x74,
|
||||
0xc7, 0xb6, 0x79, 0x24, 0x3d, 0xec, 0x31, 0x25, 0xb5, 0x59, 0x4d, 0xd4, 0x93, 0x90, 0xb4, 0x61,
|
||||
0x89, 0x5b, 0xe5, 0x33, 0x6c, 0xcc, 0x69, 0xb3, 0x98, 0x61, 0x63, 0xde, 0xff, 0x44, 0x98, 0xb8,
|
||||
0x9b, 0xea, 0x54, 0x49, 0x2d, 0xc8, 0x11, 0x6e, 0x14, 0x8d, 0x5d, 0xd3, 0x1b, 0xee, 0x05, 0xe5,
|
||||
0xe7, 0x97, 0x5e, 0x49, 0xc9, 0x31, 0x6e, 0xb2, 0x11, 0x57, 0x0a, 0xb4, 0xdd, 0xda, 0x1b, 0xb6,
|
||||
0x83, 0xea, 0x4f, 0x94, 0x3d, 0xaf, 0x04, 0x72, 0x8e, 0x3d, 0x26, 0x32, 0x48, 0xe6, 0x49, 0x1c,
|
||||
0x81, 0xa0, 0x35, 0xeb, 0x77, 0x7e, 0xfd, 0x1b, 0xad, 0x8d, 0x98, 0xe5, 0x06, 0x77, 0x3d, 0x72,
|
||||
0x8a, 0x3b, 0xd7, 0x2f, 0x90, 0x45, 0x6c, 0xe4, 0xb4, 0x9a, 0xd6, 0x7b, 0xb5, 0x41, 0x8b, 0xff,
|
||||
0x87, 0x2e, 0x2f, 0xde, 0x37, 0x3e, 0xfa, 0xd8, 0xf8, 0xe8, 0x6b, 0xe3, 0xa3, 0xd7, 0x6f, 0x7f,
|
||||
0xeb, 0xe1, 0x64, 0x91, 0xc0, 0xd2, 0x4c, 0x83, 0x58, 0xad, 0xc3, 0x65, 0xa4, 0x97, 0x49, 0xac,
|
||||
0xb2, 0x34, 0x3f, 0x33, 0x6d, 0x56, 0xe1, 0xdf, 0x5b, 0x31, 0x6d, 0xd8, 0xe6, 0xec, 0x27, 0x00,
|
||||
0x00, 0xff, 0xff, 0xe2, 0x1d, 0x6e, 0x48, 0x30, 0x02, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *AutoConfigRequest) Marshal() (dAtA []byte, err error) {
|
||||
|
@ -233,6 +281,12 @@ func (m *AutoConfigRequest) MarshalTo(dAtA []byte) (int, error) {
|
|||
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.ConsulToken)))
|
||||
i += copy(dAtA[i:], m.ConsulToken)
|
||||
}
|
||||
if len(m.CSR) > 0 {
|
||||
dAtA[i] = 0x3a
|
||||
i++
|
||||
i = encodeVarintAutoConfig(dAtA, i, uint64(len(m.CSR)))
|
||||
i += copy(dAtA[i:], m.CSR)
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
|
@ -264,6 +318,41 @@ func (m *AutoConfigResponse) MarshalTo(dAtA []byte) (int, error) {
|
|||
}
|
||||
i += n1
|
||||
}
|
||||
if m.CARoots != nil {
|
||||
dAtA[i] = 0x12
|
||||
i++
|
||||
i = encodeVarintAutoConfig(dAtA, i, uint64(m.CARoots.Size()))
|
||||
n2, err := m.CARoots.MarshalTo(dAtA[i:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i += n2
|
||||
}
|
||||
if m.Certificate != nil {
|
||||
dAtA[i] = 0x1a
|
||||
i++
|
||||
i = encodeVarintAutoConfig(dAtA, i, uint64(m.Certificate.Size()))
|
||||
n3, err := m.Certificate.MarshalTo(dAtA[i:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i += n3
|
||||
}
|
||||
if len(m.ExtraCACertificates) > 0 {
|
||||
for _, s := range m.ExtraCACertificates {
|
||||
dAtA[i] = 0x22
|
||||
i++
|
||||
l = len(s)
|
||||
for l >= 1<<7 {
|
||||
dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
|
||||
l >>= 7
|
||||
i++
|
||||
}
|
||||
dAtA[i] = uint8(l)
|
||||
i++
|
||||
i += copy(dAtA[i:], s)
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
|
@ -305,6 +394,10 @@ func (m *AutoConfigRequest) Size() (n int) {
|
|||
if l > 0 {
|
||||
n += 1 + l + sovAutoConfig(uint64(l))
|
||||
}
|
||||
l = len(m.CSR)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovAutoConfig(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
|
@ -321,6 +414,20 @@ func (m *AutoConfigResponse) Size() (n int) {
|
|||
l = m.Config.Size()
|
||||
n += 1 + l + sovAutoConfig(uint64(l))
|
||||
}
|
||||
if m.CARoots != nil {
|
||||
l = m.CARoots.Size()
|
||||
n += 1 + l + sovAutoConfig(uint64(l))
|
||||
}
|
||||
if m.Certificate != nil {
|
||||
l = m.Certificate.Size()
|
||||
n += 1 + l + sovAutoConfig(uint64(l))
|
||||
}
|
||||
if len(m.ExtraCACertificates) > 0 {
|
||||
for _, s := range m.ExtraCACertificates {
|
||||
l = len(s)
|
||||
n += 1 + l + sovAutoConfig(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
|
@ -529,6 +636,38 @@ func (m *AutoConfigRequest) Unmarshal(dAtA []byte) error {
|
|||
}
|
||||
m.ConsulToken = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
case 7:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field CSR", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowAutoConfig
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.CSR = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipAutoConfig(dAtA[iNdEx:])
|
||||
|
@ -619,6 +758,110 @@ func (m *AutoConfigResponse) Unmarshal(dAtA []byte) error {
|
|||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 2:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field CARoots", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowAutoConfig
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.CARoots == nil {
|
||||
m.CARoots = &pbconnect.CARoots{}
|
||||
}
|
||||
if err := m.CARoots.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 3:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Certificate", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowAutoConfig
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if m.Certificate == nil {
|
||||
m.Certificate = &pbconnect.IssuedCert{}
|
||||
}
|
||||
if err := m.Certificate.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 4:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field ExtraCACertificates", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowAutoConfig
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthAutoConfig
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.ExtraCACertificates = append(m.ExtraCACertificates, string(dAtA[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipAutoConfig(dAtA[iNdEx:])
|
||||
|
|
|
@ -5,6 +5,7 @@ package autoconf;
|
|||
option go_package = "github.com/hashicorp/consul/proto/pbautoconf";
|
||||
|
||||
import "proto/pbconfig/config.proto";
|
||||
import "proto/pbconnect/connect.proto";
|
||||
|
||||
// AutoConfigRequest is the data structure to be sent along with the
|
||||
// AutoConfig.InitialConfiguration RPC
|
||||
|
@ -28,9 +29,23 @@ message AutoConfigRequest {
|
|||
// ConsulToken is a Consul ACL token that the agent requesting the
|
||||
// configuration already has.
|
||||
string ConsulToken = 6;
|
||||
|
||||
// CSR is a certificate signing request to be used when generating the
|
||||
// agents TLS certificate
|
||||
string CSR = 7;
|
||||
}
|
||||
|
||||
// AutoConfigResponse is the data structure sent in response to a AutoConfig.InitialConfiguration request
|
||||
message AutoConfigResponse {
|
||||
// Config is the partial Consul configuration to inject into the agents own configuration
|
||||
config.Config Config = 1;
|
||||
|
||||
// CARoots is the current list of Connect CA Roots
|
||||
connect.CARoots CARoots = 2;
|
||||
// Certificate is the TLS certificate issued for the agent
|
||||
connect.IssuedCert Certificate = 3;
|
||||
|
||||
// ExtraCACertificates holds non-Connect certificates that may be necessary
|
||||
// to verify TLS connections with the Consul servers
|
||||
repeated string ExtraCACertificates = 4;
|
||||
}
|
|
@ -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