313 lines
10 KiB
Go
313 lines
10 KiB
Go
|
package consul
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/base64"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"time"
|
||
|
|
||
|
"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/metadata"
|
||
|
"github.com/hashicorp/consul/agent/structs"
|
||
|
"github.com/hashicorp/consul/lib"
|
||
|
"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,
|
||
|
}
|
||
|
|
||
|
// TODO (autoconf) check for JWT reuse if configured to do so.
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// Cluster endpoint is used for cluster configuration operations
|
||
|
type Cluster struct {
|
||
|
srv *Server
|
||
|
|
||
|
authorizer AutoConfigAuthorizer
|
||
|
}
|
||
|
|
||
|
// 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 (c *Cluster) updateTLSCertificatesInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||
|
if c.srv.config.AutoEncryptAllowTLS {
|
||
|
conf.AutoEncrypt = &config.AutoEncrypt{TLS: true}
|
||
|
} else {
|
||
|
conf.AutoEncrypt = &config.AutoEncrypt{TLS: false}
|
||
|
}
|
||
|
|
||
|
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 (c *Cluster) updateACLsInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||
|
acl := &config.ACL{
|
||
|
Enabled: c.srv.config.ACLsEnabled,
|
||
|
PolicyTTL: c.srv.config.ACLPolicyTTL.String(),
|
||
|
RoleTTL: c.srv.config.ACLRoleTTL.String(),
|
||
|
TokenTTL: c.srv.config.ACLTokenTTL.String(),
|
||
|
DisabledTTL: c.srv.config.ACLDisabledTTL.String(),
|
||
|
DownPolicy: c.srv.config.ACLDownPolicy,
|
||
|
DefaultPolicy: c.srv.config.ACLDefaultPolicy,
|
||
|
EnableKeyListPolicy: c.srv.config.ACLEnableKeyListPolicy,
|
||
|
}
|
||
|
|
||
|
// when ACLs are enabled we want to create a local token with a node identity
|
||
|
if c.srv.config.ACLsEnabled {
|
||
|
// we have to require local tokens or else it would require having these servers use a token with acl:write to make a
|
||
|
// token create RPC to the servers in the primary DC.
|
||
|
if !c.srv.LocalTokensEnabled() {
|
||
|
return fmt.Errorf("Agent Auto Configuration requires local token usage to be enabled in this datacenter: %s", c.srv.config.Datacenter)
|
||
|
}
|
||
|
|
||
|
// generate the accessor id
|
||
|
accessor, err := lib.GenerateUUID(c.srv.checkTokenUUID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// generate the secret id
|
||
|
secret, err := lib.GenerateUUID(c.srv.checkTokenUUID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// set up the token
|
||
|
token := structs.ACLToken{
|
||
|
AccessorID: accessor,
|
||
|
SecretID: secret,
|
||
|
Description: fmt.Sprintf("Auto Config Token for Node %q", opts.NodeName),
|
||
|
CreateTime: time.Now(),
|
||
|
Local: true,
|
||
|
NodeIdentities: []*structs.ACLNodeIdentity{
|
||
|
{
|
||
|
NodeName: opts.NodeName,
|
||
|
Datacenter: c.srv.config.Datacenter,
|
||
|
},
|
||
|
},
|
||
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||
|
}
|
||
|
|
||
|
req := structs.ACLTokenBatchSetRequest{
|
||
|
Tokens: structs.ACLTokens{&token},
|
||
|
CAS: false,
|
||
|
}
|
||
|
|
||
|
// perform the request to mint the new token
|
||
|
if _, err := c.srv.raftApplyMsgpack(structs.ACLTokenSetRequestType, &req); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
acl.Tokens = &config.ACLTokens{Agent: secret}
|
||
|
}
|
||
|
|
||
|
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 (c *Cluster) updateJoinAddressesInConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||
|
members, err := c.srv.LANSegmentMembers(opts.SegmentName)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
var joinAddrs []string
|
||
|
for _, m := range members {
|
||
|
if ok, _ := metadata.IsConsulServer(m); ok {
|
||
|
serfAddr := net.TCPAddr{IP: m.Addr, Port: int(m.Port)}
|
||
|
joinAddrs = append(joinAddrs, serfAddr.String())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if conf.Gossip == nil {
|
||
|
conf.Gossip = &config.Gossip{}
|
||
|
}
|
||
|
|
||
|
conf.Gossip.RetryJoinLAN = joinAddrs
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// updateGossipEncryptionInConfig will populate the gossip encryption configuration settings
|
||
|
func (c *Cluster) updateGossipEncryptionInConfig(_ AutoConfigOptions, conf *config.Config) error {
|
||
|
// Add gossip encryption settings if there is any key loaded
|
||
|
memberlistConfig := c.srv.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 (c *Cluster) updateTLSSettingsInConfig(_ AutoConfigOptions, conf *config.Config) error {
|
||
|
// add in TLS configuration
|
||
|
if conf.TLS == nil {
|
||
|
conf.TLS = &config.TLS{}
|
||
|
}
|
||
|
conf.TLS.VerifyServerHostname = c.srv.tlsConfigurator.VerifyServerHostname()
|
||
|
base := c.srv.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 (c *Cluster) baseConfig(opts AutoConfigOptions, conf *config.Config) error {
|
||
|
if opts.NodeName == "" {
|
||
|
return fmt.Errorf("Cannot generate auto config response without a node name")
|
||
|
}
|
||
|
|
||
|
conf.Datacenter = c.srv.config.Datacenter
|
||
|
conf.PrimaryDatacenter = c.srv.config.PrimaryDatacenter
|
||
|
conf.NodeName = opts.NodeName
|
||
|
conf.SegmentName = opts.SegmentName
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
type autoConfigUpdater func(c *Cluster, 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{
|
||
|
(*Cluster).baseConfig,
|
||
|
(*Cluster).updateJoinAddressesInConfig,
|
||
|
(*Cluster).updateGossipEncryptionInConfig,
|
||
|
(*Cluster).updateTLSSettingsInConfig,
|
||
|
(*Cluster).updateACLsInConfig,
|
||
|
(*Cluster).updateTLSCertificatesInConfig,
|
||
|
}
|
||
|
)
|
||
|
|
||
|
// AgentAutoConfig will authorize the incoming request and then generate the configuration
|
||
|
// to push down to the client
|
||
|
func (c *Cluster) AutoConfig(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 = c.srv.config.Datacenter
|
||
|
}
|
||
|
|
||
|
// TODO (autoconf) Is performing auto configuration over the WAN really a bad idea?
|
||
|
if req.Datacenter != c.srv.config.Datacenter {
|
||
|
return fmt.Errorf("invalid datacenter %q - agent auto configuration cannot target a remote datacenter", req.Datacenter)
|
||
|
}
|
||
|
|
||
|
// forward to the leader
|
||
|
if done, err := c.srv.forward("Cluster.AutoConfig", req, req, resp); done {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// TODO (autoconf) maybe panic instead?
|
||
|
if c.authorizer == nil {
|
||
|
return fmt.Errorf("No Auto Config authorizer is configured")
|
||
|
}
|
||
|
|
||
|
// authorize the request with the configured authorizer
|
||
|
opts, err := c.authorizer.Authorize(req)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
conf := &config.Config{}
|
||
|
|
||
|
// update all the configurations
|
||
|
for _, configFn := range autoConfigUpdaters {
|
||
|
if err := configFn(c, opts, conf); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
resp.Config = conf
|
||
|
return nil
|
||
|
}
|