Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Agent Auto Configuration: Configuration Syntax Updates #8003

Merged
merged 1 commit into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions agent/config/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ import (
"github.com/hashicorp/consul/agent/checks"
"github.com/hashicorp/consul/agent/connect/ca"
"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/lib"
libtempl "github.com/hashicorp/consul/lib/template"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-sockaddr/template"
"github.com/hashicorp/memberlist"
Expand Down Expand Up @@ -905,6 +909,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
AutoEncryptDNSSAN: autoEncryptDNSSAN,
AutoEncryptIPSAN: autoEncryptIPSAN,
AutoEncryptAllowTLS: autoEncryptAllowTLS,
AutoConfig: b.autoConfigVal(c.AutoConfig),
ConnectEnabled: connectEnabled,
ConnectCAProvider: connectCAProvider,
ConnectCAConfig: connectCAConfig,
Expand Down Expand Up @@ -1285,6 +1290,10 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
return err
}

if err := b.validateAutoConfig(rt); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -1918,6 +1927,142 @@ func (b *Builder) isUnixAddr(a net.Addr) bool {
return a != nil && ok
}

func (b *Builder) autoConfigVal(raw AutoConfigRaw) AutoConfig {
var val AutoConfig

val.Enabled = b.boolValWithDefault(raw.Enabled, false)
val.IntroToken = b.stringVal(raw.IntroToken)
val.IntroTokenFile = b.stringVal(raw.IntroTokenFile)
// These can be go-discover values and so don't have to resolve fully yet
val.ServerAddresses = b.expandAllOptionalAddrs("auto_config.server_addresses", raw.ServerAddresses)
val.DNSSANs = raw.DNSSANs

for _, i := range raw.IPSANs {
ip := net.ParseIP(i)
if ip == nil {
b.warn(fmt.Sprintf("Cannot parse ip %q from auto_config.ip_sans", i))
continue
}
val.IPSANs = append(val.IPSANs, ip)
}

val.Authorizer = b.autoConfigAuthorizerVal(raw.Authorizer)

return val
}

func (b *Builder) autoConfigAuthorizerVal(raw AutoConfigAuthorizerRaw) AutoConfigAuthorizer {
var val AutoConfigAuthorizer

val.Enabled = b.boolValWithDefault(raw.Enabled, false)
val.ClaimAssertions = raw.ClaimAssertions
val.AllowReuse = b.boolValWithDefault(raw.AllowReuse, false)
val.AuthMethod = structs.ACLAuthMethod{
Name: "Auto Config Authorizer",
Type: "jwt",
// TODO (autoconf) - Configurable token TTL
MaxTokenTTL: 72 * time.Hour,
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
Config: map[string]interface{}{
"JWTSupportedAlgs": raw.JWTSupportedAlgs,
"BoundAudiences": raw.BoundAudiences,
"ClaimMappings": raw.ClaimMappings,
"ListClaimMappings": raw.ListClaimMappings,
"OIDCDiscoveryURL": b.stringVal(raw.OIDCDiscoveryURL),
"OIDCDiscoveryCACert": b.stringVal(raw.OIDCDiscoveryCACert),
"JWKSURL": b.stringVal(raw.JWKSURL),
"JWKSCACert": b.stringVal(raw.JWKSCACert),
"JWTValidationPubKeys": raw.JWTValidationPubKeys,
"BoundIssuer": b.stringVal(raw.BoundIssuer),
"ExpirationLeeway": b.durationVal("auto_config.authorizer.expiration_leeway", raw.ExpirationLeeway),
"NotBeforeLeeway": b.durationVal("auto_config.authorizer.not_before_leeway", raw.NotBeforeLeeway),
"ClockSkewLeeway": b.durationVal("auto_config.authorizer.clock_skew_leeway", raw.ClockSkewLeeway),
},
// should be unnecessary as we aren't using the typical login process to create tokens but this is our
// desired mode regardless so if it ever did matter its probably better to be explicit.
TokenLocality: "local",
}

return val
}

func (b *Builder) validateAutoConfig(rt RuntimeConfig) error {
autoconf := rt.AutoConfig

if err := b.validateAutoConfigAuthorizer(rt); err != nil {
return err
}

if !autoconf.Enabled {
return nil
}

// Auto Config doesn't currently support configuring servers
if rt.ServerMode {
return fmt.Errorf("auto_config.enabled cannot be set to true for server agents.")
}

// When both are set we will prefer the given value over the file.
if autoconf.IntroToken != "" && autoconf.IntroTokenFile != "" {
b.warn("auto_config.intro_token and auto_config.intro_token_file are both set. Using the value of auto_config.intro_token")
} else if autoconf.IntroToken == "" && autoconf.IntroTokenFile == "" {
return fmt.Errorf("one of auto_config.intro_token or auto_config.intro_token_file must be set to enable auto_config")
}

if len(autoconf.ServerAddresses) == 0 {
// TODO (autoconf) can we/should we infer this from the join/retry join addresses. I think no, as we will potentially
// be overriding those retry join addresses with the autoconf process anyways.
return fmt.Errorf("auto_config.enabled is set without providing a list of addresses")
}

// TODO (autoconf) should we validate the DNS and IP SANs? The IP SANs have already been parsed into IPs
return nil
}

func (b *Builder) validateAutoConfigAuthorizer(rt RuntimeConfig) error {
authz := rt.AutoConfig.Authorizer

if !authz.Enabled {
return nil
}
// Auto Config Authorization is only supported on servers
if !rt.ServerMode {
return fmt.Errorf("auto_config.authorizer.enabled cannot be set to true for client agents")
}

// build out the validator to ensure that the given configuration was valid
null := hclog.NewNullLogger()
validator, err := ssoauth.NewValidator(null, &authz.AuthMethod)

if err != nil {
return fmt.Errorf("auto_config.authorizer has invalid configuration: %v", err)
}

// create a blank identity for use to validate the claim assertions.
blankID := validator.NewIdentity()
varMap := map[string]string{
"node": "fake",
"segment": "fake",
}

// validate all the claim assertions
for _, raw := range authz.ClaimAssertions {
// validate any HIL
filled, err := libtempl.InterpolateHIL(raw, varMap, true)
if err != nil {
return fmt.Errorf("auto_config.claim_assertion %q is invalid: %v", raw, err)
}

// validate the bexpr syntax - note that for now all the keys mapped by the claim mappings
// are not validateable due to them being put inside a map. Some bexpr updates to setup keys
// from current map keys would probably be nice here.
if _, err := bexpr.CreateEvaluatorForType(filled, nil, blankID.SelectableFields); err != nil {
return fmt.Errorf("auto_config.claim_assertion %q is invalid: %v", raw, err)
}
}
return nil
}

// decodeBytes returns the encryption key decoded.
func decodeBytes(key string) ([]byte, error) {
return base64.StdEncoding.DecodeString(key)
Expand Down
32 changes: 32 additions & 0 deletions agent/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type Config struct {
AdvertiseAddrWAN *string `json:"advertise_addr_wan,omitempty" hcl:"advertise_addr_wan" mapstructure:"advertise_addr_wan"`
AdvertiseAddrWANIPv4 *string `json:"advertise_addr_wan_ipv4,omitempty" hcl:"advertise_addr_wan_ipv4" mapstructure:"advertise_addr_wan_ipv4"`
AdvertiseAddrWANIPv6 *string `json:"advertise_addr_wan_ipv6,omitempty" hcl:"advertise_addr_wan_ipv6" mapstructure:"advertise_addr_ipv6"`
AutoConfig AutoConfigRaw `json:"auto_config,omitempty" hcl:"auto_config" mapstructure:"auto_config"`
Autopilot Autopilot `json:"autopilot,omitempty" hcl:"autopilot" mapstructure:"autopilot"`
BindAddr *string `json:"bind_addr,omitempty" hcl:"bind_addr" mapstructure:"bind_addr"`
Bootstrap *bool `json:"bootstrap,omitempty" hcl:"bootstrap" mapstructure:"bootstrap"`
Expand Down Expand Up @@ -693,3 +694,34 @@ type AuditSink struct {
RotateDuration *string `json:"rotate_duration,omitempty" hcl:"rotate_duration" mapstructure:"rotate_duration"`
RotateMaxFiles *int `json:"rotate_max_files,omitempty" hcl:"rotate_max_files" mapstructure:"rotate_max_files"`
}

type AutoConfigRaw struct {
Enabled *bool `json:"enabled,omitempty" hcl:"enabled" mapstructure:"enabled"`
IntroToken *string `json:"intro_token,omitempty" hcl:"intro_token" mapstructure:"intro_token"`
IntroTokenFile *string `json:"intro_token_file,omitempty" hcl:"intro_token_file" mapstructure:"intro_token_file"`
mkeeler marked this conversation as resolved.
Show resolved Hide resolved
ServerAddresses []string `json:"server_addresses,omitempty" hcl:"server_addresses" mapstructure:"server_addresses"`
DNSSANs []string `json:"dns_sans,omitempty" hcl:"dns_sans" mapstructure:"dns_sans"`
IPSANs []string `json:"ip_sans,omitempty" hcl:"ip_sans" mapstructure:"ip_sans"`
Authorizer AutoConfigAuthorizerRaw `json:"authorizer,omitempty" hcl:"authorizer" mapstructure:"authorizer"`
}

type AutoConfigAuthorizerRaw struct {
Enabled *bool `json:"enabled,omitempty" hcl:"enabled" mapstructure:"enabled"`
ClaimAssertions []string `json:"claim_assertions,omitempty" hcl:"claim_assertions" mapstructure:"claim_assertions"`
AllowReuse *bool `json:"allow_reuse,omitempty" hcl:"allow_reuse" mapstructure:"allow_reuse"`

// Fields to be shared with the JWT Auth Method
JWTSupportedAlgs []string `json:"jwt_supported_algs,omitempty" hcl:"jwt_supported_algs" mapstructure:"jwt_supported_algs"`
BoundAudiences []string `json:"bound_audiences,omitempty" hcl:"bound_audiences" mapstructure:"bound_audiences"`
ClaimMappings map[string]string `json:"claim_mappings,omitempty" hcl:"claim_mappings" mapstructure:"claim_mappings"`
ListClaimMappings map[string]string `json:"list_claim_mappings,omitempty" hcl:"list_claim_mappings" mapstructure:"list_claim_mappings"`
OIDCDiscoveryURL *string `json:"oidc_discovery_url,omitempty" hcl:"oidc_discovery_url" mapstructure:"oidc_discovery_url"`
OIDCDiscoveryCACert *string `json:"oidc_discovery_ca_cert,omitempty" hcl:"oidc_discovery_ca_cert" mapstructure:"oidc_discovery_ca_cert"`
JWKSURL *string `json:"jwks_url,omitempty" hcl:"jwks_url" mapstructure:"jwks_url"`
JWKSCACert *string `json:"jwks_ca_cert,omitempty" hcl:"jwks_ca_cert" mapstructure:"jwks_ca_cert"`
JWTValidationPubKeys []string `json:"jwt_validation_pub_keys,omitempty" hcl:"jwt_validation_pub_keys" mapstructure:"jwt_validation_pub_keys"`
BoundIssuer *string `json:"bound_issuer,omitempty" hcl:"bound_issuer" mapstructure:"bound_issuer"`
ExpirationLeeway *string `json:"expiration_leeway,omitempty" hcl:"expiration_leeway" mapstructure:"expiration_leeway"`
NotBeforeLeeway *string `json:"not_before_leeway,omitempty" hcl:"not_before_leeway" mapstructure:"not_before_leeway"`
ClockSkewLeeway *string `json:"clock_skew_leeway,omitempty" hcl:"clock_skew_leeway" mapstructure:"clock_skew_leeway"`
}
8 changes: 8 additions & 0 deletions agent/config/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ func merge(a, b interface{}) interface{} {
func mergeValue(a, b reflect.Value) reflect.Value {
switch a.Kind() {
case reflect.Map:
// dont bother allocating a new map to aggregate keys in when either one
// or both of the maps to merge is the zero value - nil
if a.IsZero() {
return b
} else if b.IsZero() {
return a
}

r := reflect.MakeMap(a.Type())
for _, k := range a.MapKeys() {
v := a.MapIndex(k)
Expand Down
7 changes: 1 addition & 6 deletions agent/config/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,7 @@ func TestMerge(t *testing.T) {
"a": "b",
"c": "e",
},
Ports: Ports{DNS: pInt(2), HTTP: pInt(3)},
SnapshotAgent: map[string]interface{}{},
TaggedAddresses: map[string]string{},
HTTPConfig: HTTPConfig{ResponseHeaders: map[string]string{}},
DNS: DNS{ServiceTTL: map[string]string{}},
Connect: Connect{CAConfig: map[string]interface{}{}},
Ports: Ports{DNS: pInt(2), HTTP: pInt(3)},
},
},
}
Expand Down
26 changes: 26 additions & 0 deletions agent/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,10 @@ type RuntimeConfig struct {
// AutoEncrypt.Sign requests.
AutoEncryptAllowTLS bool

// AutoConfig is a grouping of the configurations around the agent auto configuration
// process including how servers can authorize requests.
AutoConfig AutoConfig

// ConnectEnabled opts the agent into connect. It should be set on all clients
// and servers in a cluster for correct connect operation.
ConnectEnabled bool
Expand Down Expand Up @@ -1566,6 +1570,24 @@ type RuntimeConfig struct {
EnterpriseRuntimeConfig
}

type AutoConfig struct {
Enabled bool
IntroToken string
IntroTokenFile string
ServerAddresses []string
DNSSANs []string
IPSANs []net.IP
Authorizer AutoConfigAuthorizer
}
mkeeler marked this conversation as resolved.
Show resolved Hide resolved

type AutoConfigAuthorizer struct {
Enabled bool
AuthMethod structs.ACLAuthMethod
// AuthMethodConfig ssoauth.Config
ClaimAssertions []string
AllowReuse bool
}

func (c *RuntimeConfig) apiAddresses(maxPerType int) (unixAddrs, httpAddrs, httpsAddrs []string) {
if len(c.HTTPSAddrs) > 0 {
for i, addr := range c.HTTPSAddrs {
Expand Down Expand Up @@ -1729,6 +1751,10 @@ func (c *RuntimeConfig) ToTLSUtilConfig() tlsutil.Config {
// isSecret determines whether a field name represents a field which
// may contain a secret.
func isSecret(name string) bool {
// special cases for AuthMethod locality and intro token file
if name == "TokenLocality" || name == "IntroTokenFile" {
return false
}
name = strings.ToLower(name)
return strings.Contains(name, "key") || strings.Contains(name, "token") || strings.Contains(name, "secret")
}
Expand Down
Loading