open-consul/agent/consul/auto_config_endpoint.go

305 lines
10 KiB
Go
Raw Normal View History

package consul
import (
"context"
"encoding/base64"
"fmt"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/agentpb"
"github.com/hashicorp/consul/agent/agentpb/config"
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib/template"
"github.com/hashicorp/consul/tlsutil"
bexpr "github.com/hashicorp/go-bexpr"
)
type AutoConfigOptions struct {
NodeName string
SegmentName string
}
type AutoConfigAuthorizer interface {
// Authorizes the request and returns a struct containing the various
// options for how to generate the configuration.
Authorize(*agentpb.AutoConfigRequest) (AutoConfigOptions, error)
}
type disabledAuthorizer struct{}
func (_ *disabledAuthorizer) Authorize(_ *agentpb.AutoConfigRequest) (AutoConfigOptions, error) {
return AutoConfigOptions{}, fmt.Errorf("Auto Config is disabled")
}
type jwtAuthorizer struct {
validator *ssoauth.Validator
allowReuse bool
claimAssertions []string
}
func (a *jwtAuthorizer) Authorize(req *agentpb.AutoConfigRequest) (AutoConfigOptions, error) {
// perform basic JWT Authorization
identity, err := a.validator.ValidateLogin(context.Background(), req.JWT)
if err != nil {
// TODO (autoconf) maybe we should add a more generic permission denied error not tied to the ACL package?
return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err)
}
varMap := map[string]string{
"node": req.Node,
"segment": req.Segment,
}
for _, raw := range a.claimAssertions {
// validate and fill any HIL
filled, err := template.InterpolateHIL(raw, varMap, true)
if err != nil {
return AutoConfigOptions{}, fmt.Errorf("Failed to render claim assertion template %q: %w", raw, err)
}
evaluator, err := bexpr.CreateEvaluatorForType(filled, nil, identity.SelectableFields)
if err != nil {
return AutoConfigOptions{}, fmt.Errorf("Failed to create evaluator for claim assertion %q: %w", filled, err)
}
ok, err := evaluator.Evaluate(identity.SelectableFields)
if err != nil {
return AutoConfigOptions{}, fmt.Errorf("Failed to execute claim assertion %q: %w", filled, err)
}
if !ok {
return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT claim assertion")
}
}
return AutoConfigOptions{
NodeName: req.Node,
SegmentName: req.Segment,
}, 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)
}
// AutoConfig endpoint is used for cluster auto configuration operations
type AutoConfig struct {
// currently AutoConfig does not support pushing down any configuration that would be reloadable on the servers
// (outside of some TLS settings such as the configured CA certs which are retrieved via the TLS configurator)
// If that changes then we will need to change this to use an atomic.Value and provide means of reloading it.
config *Config
tlsConfigurator *tlsutil.Configurator
backend AutoConfigBackend
authorizer AutoConfigAuthorizer
}
func NewAutoConfig(conf *Config, tlsConfigurator *tlsutil.Configurator, backend AutoConfigBackend, authz AutoConfigAuthorizer) *AutoConfig {
if conf == nil {
conf = DefaultConfig()
}
return &AutoConfig{
config: conf,
tlsConfigurator: tlsConfigurator,
backend: backend,
authorizer: authz,
}
}
// updateTLSCertificatesInConfig will ensure that the TLS settings regarding how an agent is
// 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}
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{
Enabled: ac.config.ACLsEnabled,
PolicyTTL: ac.config.ACLPolicyTTL.String(),
RoleTTL: ac.config.ACLRoleTTL.String(),
TokenTTL: ac.config.ACLTokenTTL.String(),
DisabledTTL: ac.config.ACLDisabledTTL.String(),
DownPolicy: ac.config.ACLDownPolicy,
DefaultPolicy: ac.config.ACLDefaultPolicy,
EnableKeyListPolicy: ac.config.ACLEnableKeyListPolicy,
}
// when ACLs are enabled we want to create a local token with a node identity
if ac.config.ACLsEnabled {
// set up the token template - the ids and create
template := structs.ACLToken{
Description: fmt.Sprintf("Auto Config Token for Node %q", opts.NodeName),
Local: true,
NodeIdentities: []*structs.ACLNodeIdentity{
{
NodeName: opts.NodeName,
Datacenter: ac.config.Datacenter,
},
},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}
token, err := ac.backend.CreateACLToken(&template)
if err != nil {
return fmt.Errorf("Failed to generate an ACL token for node %q - %w", opts.NodeName, err)
}
acl.Tokens = &config.ACLTokens{Agent: token.SecretID}
}
conf.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 {
joinAddrs, err := ac.backend.DatacenterJoinAddresses(opts.SegmentName)
if err != nil {
return err
}
if conf.Gossip == nil {
conf.Gossip = &config.Gossip{}
}
conf.Gossip.RetryJoinLAN = joinAddrs
return nil
}
// updateGossipEncryptionInConfig will populate the gossip encryption configuration settings
func (ac *AutoConfig) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *config.Config) 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 conf.Gossip.Encryption == nil {
conf.Gossip.Encryption = &config.GossipEncryption{}
}
pk := lanKeyring.GetPrimaryKey()
if len(pk) > 0 {
conf.Gossip.Encryption.Key = base64.StdEncoding.EncodeToString(pk)
}
conf.Gossip.Encryption.VerifyIncoming = memberlistConfig.GossipVerifyIncoming
conf.Gossip.Encryption.VerifyOutgoing = memberlistConfig.GossipVerifyOutgoing
}
return nil
}
// updateTLSSettingsInConfig will populate the TLS configuration settings but will not
// populate leaf or ca certficiates.
func (ac *AutoConfig) updateTLSSettingsInConfig(_ AutoConfigOptions, conf *config.Config) error {
if ac.tlsConfigurator == nil {
// TLS is not enabled?
return nil
}
// add in TLS configuration
if conf.TLS == nil {
conf.TLS = &config.TLS{}
}
conf.TLS.VerifyServerHostname = ac.tlsConfigurator.VerifyServerHostname()
base := ac.tlsConfigurator.Base()
conf.TLS.VerifyOutgoing = base.VerifyOutgoing
conf.TLS.MinVersion = base.TLSMinVersion
conf.TLS.PreferServerCipherSuites = base.PreferServerCipherSuites
var err error
conf.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 {
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
return nil
}
type autoConfigUpdater func(c *AutoConfig, opts AutoConfigOptions, conf *config.Config) error
var (
// variable holding the list of config updating functions to execute when generating
// the auto config response. This will allow for more easily adding extra self-contained
// configurators here in the future.
autoConfigUpdaters []autoConfigUpdater = []autoConfigUpdater{
(*AutoConfig).baseConfig,
(*AutoConfig).updateJoinAddressesInConfig,
(*AutoConfig).updateGossipEncryptionInConfig,
(*AutoConfig).updateTLSSettingsInConfig,
(*AutoConfig).updateACLsInConfig,
(*AutoConfig).updateTLSCertificatesInConfig,
}
)
// AgentAutoConfig will authorize the incoming request and then generate the configuration
// to push down to the client
func (ac *AutoConfig) InitialConfiguration(req *agentpb.AutoConfigRequest, resp *agentpb.AutoConfigResponse) error {
// default the datacenter to our datacenter - agents do not have to specify this as they may not
// yet know the datacenter name they are going to be in.
if req.Datacenter == "" {
req.Datacenter = ac.config.Datacenter
}
// TODO (autoconf) Is performing auto configuration over the WAN really a bad idea?
if req.Datacenter != ac.config.Datacenter {
return fmt.Errorf("invalid datacenter %q - agent auto configuration cannot target a remote datacenter", req.Datacenter)
}
// TODO (autoconf) maybe panic instead?
if ac.backend == nil {
return fmt.Errorf("No Auto Config backend is configured")
}
// forward to the leader
if done, err := ac.backend.ForwardRPC("AutoConfig.InitialConfiguration", req, req, resp); done {
return err
}
// TODO (autoconf) maybe panic instead?
if ac.authorizer == nil {
return fmt.Errorf("No Auto Config authorizer is configured")
}
// authorize the request with the configured authorizer
opts, err := ac.authorizer.Authorize(req)
if err != nil {
return err
}
conf := &config.Config{}
// update all the configurations
for _, configFn := range autoConfigUpdaters {
if err := configFn(ac, opts, conf); err != nil {
return err
}
}
resp.Config = conf
return nil
}