Skip to content

Commit

Permalink
connect: include optional partition prefixes in SPIFFE identifiers (#…
Browse files Browse the repository at this point in the history
…10507)

NOTE: this does not include any intentions enforcement changes yet
  • Loading branch information
rboyer authored Jun 25, 2021
1 parent 1c28aa7 commit ed8a901
Show file tree
Hide file tree
Showing 18 changed files with 275 additions and 114 deletions.
3 changes: 3 additions & 0 deletions .changelog/10507.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
connect: include optional partition prefixes in SPIFFE identifiers
```
1 change: 1 addition & 0 deletions agent/auto-config/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ func (ac *AutoConfig) generateCSR() (csr string, key string, err error) {
Host: unknownTrustDomain,
Datacenter: ac.config.Datacenter,
Agent: ac.config.NodeName,
// TODO(rb)(partitions): populate the partition field from the agent config
}

caConfig, err := ac.config.ConnectCAConfiguration()
Expand Down
6 changes: 6 additions & 0 deletions agent/cache-types/connect_ca_leaf.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ func (c *ConnectCALeaf) generateNewLeaf(req *ConnectCALeafRequest,
id = &connect.SpiffeIDService{
Host: roots.TrustDomain,
Datacenter: req.Datacenter,
Partition: req.TargetPartition(),
Namespace: req.TargetNamespace(),
Service: req.Service,
}
Expand All @@ -532,6 +533,7 @@ func (c *ConnectCALeaf) generateNewLeaf(req *ConnectCALeafRequest,
id = &connect.SpiffeIDAgent{
Host: roots.TrustDomain,
Datacenter: req.Datacenter,
Partition: req.TargetPartition(),
Agent: req.Agent,
}
dnsNames = append([]string{"localhost"}, req.DNSSAN...)
Expand Down Expand Up @@ -676,6 +678,10 @@ func (r *ConnectCALeafRequest) Key() string {
return ""
}

func (req *ConnectCALeafRequest) TargetPartition() string {
return req.PartitionOrDefault()
}

func (r *ConnectCALeafRequest) CacheInfo() cache.RequestInfo {
return cache.RequestInfo{
Token: r.Token,
Expand Down
34 changes: 22 additions & 12 deletions agent/connect/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ type CertURI interface {

var (
spiffeIDServiceRegexp = regexp.MustCompile(
`^/ns/([^/]+)/dc/([^/]+)/svc/([^/]+)$`)
`^(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/]+)$`)
spiffeIDAgentRegexp = regexp.MustCompile(
`^/agent/client/dc/([^/]+)/id/([^/]+)$`)
`^(?:/ap/([^/]+))?/agent/client/dc/([^/]+)/id/([^/]+)$`)
)

// ParseCertURIFromString attempts to parse a string representation of a
Expand Down Expand Up @@ -56,24 +56,29 @@ func ParseCertURI(input *url.URL) (CertURI, error) {
// Determine the values. We assume they're sane to save cycles,
// but if the raw path is not empty that means that something is
// URL encoded so we go to the slow path.
ns := v[1]
dc := v[2]
service := v[3]
ap := v[1]
ns := v[2]
dc := v[3]
service := v[4]
if input.RawPath != "" {
var err error
if ns, err = url.PathUnescape(v[1]); err != nil {
if ap, err = url.PathUnescape(v[1]); err != nil {
return nil, fmt.Errorf("Invalid admin partition: %s", err)
}
if ns, err = url.PathUnescape(v[2]); err != nil {
return nil, fmt.Errorf("Invalid namespace: %s", err)
}
if dc, err = url.PathUnescape(v[2]); err != nil {
if dc, err = url.PathUnescape(v[3]); err != nil {
return nil, fmt.Errorf("Invalid datacenter: %s", err)
}
if service, err = url.PathUnescape(v[3]); err != nil {
if service, err = url.PathUnescape(v[4]); err != nil {
return nil, fmt.Errorf("Invalid service: %s", err)
}
}

return &SpiffeIDService{
Host: input.Host,
Partition: ap,
Namespace: ns,
Datacenter: dc,
Service: service,
Expand All @@ -82,20 +87,25 @@ func ParseCertURI(input *url.URL) (CertURI, error) {
// Determine the values. We assume they're sane to save cycles,
// but if the raw path is not empty that means that something is
// URL encoded so we go to the slow path.
dc := v[1]
agent := v[2]
ap := v[1]
dc := v[2]
agent := v[3]
if input.RawPath != "" {
var err error
if dc, err = url.PathUnescape(v[1]); err != nil {
if ap, err = url.PathUnescape(v[1]); err != nil {
return nil, fmt.Errorf("Invalid admin partition: %s", err)
}
if dc, err = url.PathUnescape(v[2]); err != nil {
return nil, fmt.Errorf("Invalid datacenter: %s", err)
}
if agent, err = url.PathUnescape(v[2]); err != nil {
if agent, err = url.PathUnescape(v[3]); err != nil {
return nil, fmt.Errorf("Invalid node: %s", err)
}
}

return &SpiffeIDAgent{
Host: input.Host,
Partition: ap,
Datacenter: dc,
Agent: agent,
}, nil
Expand Down
12 changes: 9 additions & 3 deletions agent/connect/uri_agent.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
package connect

import (
"fmt"
"net/url"

"github.com/hashicorp/consul/agent/structs"
)

// SpiffeIDService is the structure to represent the SPIFFE ID for an agent.
type SpiffeIDAgent struct {
Host string
Partition string
Datacenter string
Agent string
}

func (id SpiffeIDAgent) PartitionOrDefault() string {
return structs.PartitionOrDefault(id.Partition)
}

// URI returns the *url.URL for this SPIFFE ID.
func (id *SpiffeIDAgent) URI() *url.URL {
func (id SpiffeIDAgent) URI() *url.URL {
var result url.URL
result.Scheme = "spiffe"
result.Host = id.Host
result.Path = fmt.Sprintf("/agent/client/dc/%s/id/%s", id.Datacenter, id.Agent)
result.Path = id.uriPath()
return &result
}
9 changes: 9 additions & 0 deletions agent/connect/uri_agent_oss.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// +build !consulent

package connect

import "fmt"

func (id SpiffeIDAgent) uriPath() string {
return fmt.Sprintf("/agent/client/dc/%s/id/%s", id.Datacenter, id.Agent)
}
32 changes: 32 additions & 0 deletions agent/connect/uri_agent_oss_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// +build !consulent

package connect

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestSpiffeIDAgentURI(t *testing.T) {
t.Run("default partition", func(t *testing.T) {
agent := &SpiffeIDAgent{
Host: "1234.consul",
Datacenter: "dc1",
Agent: "123",
}

require.Equal(t, "spiffe://1234.consul/agent/client/dc/dc1/id/123", agent.URI().String())
})

t.Run("partitions are ignored", func(t *testing.T) {
agent := &SpiffeIDAgent{
Host: "1234.consul",
Partition: "foobar",
Datacenter: "dc1",
Agent: "123",
}

require.Equal(t, "spiffe://1234.consul/agent/client/dc/dc1/id/123", agent.URI().String())
})
}
17 changes: 0 additions & 17 deletions agent/connect/uri_agent_test.go

This file was deleted.

21 changes: 17 additions & 4 deletions agent/connect/uri_service.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
package connect

import (
"fmt"
"net/url"

"github.com/hashicorp/consul/agent/structs"
)

// SpiffeIDService is the structure to represent the SPIFFE ID for a service.
type SpiffeIDService struct {
Host string
Partition string
Namespace string
Datacenter string
Service string
}

func (id SpiffeIDService) NamespaceOrDefault() string {
return structs.NamespaceOrDefault(id.Namespace)
}

func (id SpiffeIDService) MatchesPartition(partition string) bool {
return id.PartitionOrDefault() == structs.PartitionOrDefault(partition)
}

func (id SpiffeIDService) PartitionOrDefault() string {
return structs.PartitionOrDefault(id.Partition)
}

// URI returns the *url.URL for this SPIFFE ID.
func (id *SpiffeIDService) URI() *url.URL {
func (id SpiffeIDService) URI() *url.URL {
var result url.URL
result.Scheme = "spiffe"
result.Host = id.Host
result.Path = fmt.Sprintf("/ns/%s/dc/%s/svc/%s",
id.Namespace, id.Datacenter, id.Service)
result.Path = id.uriPath()
return &result
}
12 changes: 11 additions & 1 deletion agent/connect/uri_service_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@
package connect

import (
"fmt"

"github.com/hashicorp/consul/agent/structs"
)

// GetEnterpriseMeta will synthesize an EnterpriseMeta struct from the SpiffeIDService.
// in OSS this just returns an empty (but never nil) struct pointer
func (id *SpiffeIDService) GetEnterpriseMeta() *structs.EnterpriseMeta {
func (id SpiffeIDService) GetEnterpriseMeta() *structs.EnterpriseMeta {
return &structs.EnterpriseMeta{}
}

func (id SpiffeIDService) uriPath() string {
return fmt.Sprintf("/ns/%s/dc/%s/svc/%s",
id.NamespaceOrDefault(),
id.Datacenter,
id.Service,
)
}
40 changes: 40 additions & 0 deletions agent/connect/uri_service_oss_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// +build !consulent

package connect

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestSpiffeIDServiceURI(t *testing.T) {
t.Run("default partition; default namespace", func(t *testing.T) {
svc := &SpiffeIDService{
Host: "1234.consul",
Datacenter: "dc1",
Service: "web",
}
require.Equal(t, "spiffe://1234.consul/ns/default/dc/dc1/svc/web", svc.URI().String())
})

t.Run("partitions are ignored", func(t *testing.T) {
svc := &SpiffeIDService{
Host: "1234.consul",
Partition: "other",
Datacenter: "dc1",
Service: "web",
}
require.Equal(t, "spiffe://1234.consul/ns/default/dc/dc1/svc/web", svc.URI().String())
})

t.Run("namespaces are ignored", func(t *testing.T) {
svc := &SpiffeIDService{
Host: "1234.consul",
Namespace: "other",
Datacenter: "dc1",
Service: "web",
}
require.Equal(t, "spiffe://1234.consul/ns/default/dc/dc1/svc/web", svc.URI().String())
})
}
6 changes: 3 additions & 3 deletions agent/connect/uri_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ type SpiffeIDSigning struct {
}

// URI returns the *url.URL for this SPIFFE ID.
func (id *SpiffeIDSigning) URI() *url.URL {
func (id SpiffeIDSigning) URI() *url.URL {
var result url.URL
result.Scheme = "spiffe"
result.Host = id.Host()
return &result
}

// Host is the canonical representation as a DNS-compatible hostname.
func (id *SpiffeIDSigning) Host() string {
func (id SpiffeIDSigning) Host() string {
return strings.ToLower(fmt.Sprintf("%s.%s", id.ClusterID, id.Domain))
}

Expand All @@ -36,7 +36,7 @@ func (id *SpiffeIDSigning) Host() string {
// method on CertURI interface since we don't intend this to be extensible
// outside and it's easier to reason about the security properties when they are
// all in one place with "allowlist" semantics.
func (id *SpiffeIDSigning) CanSign(cu CertURI) bool {
func (id SpiffeIDSigning) CanSign(cu CertURI) bool {
switch other := cu.(type) {
case *SpiffeIDSigning:
// We can only sign other CA certificates for the same trust domain. Note
Expand Down
8 changes: 4 additions & 4 deletions agent/connect/uri_signing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,25 @@ func TestSpiffeIDSigning_CanSign(t *testing.T) {
{
name: "service - good",
id: testSigning,
input: &SpiffeIDService{TestClusterID + ".consul", "default", "dc1", "web"},
input: &SpiffeIDService{Host: TestClusterID + ".consul", Namespace: "default", Datacenter: "dc1", Service: "web"},
want: true,
},
{
name: "service - good midex case",
id: testSigning,
input: &SpiffeIDService{strings.ToUpper(TestClusterID) + ".CONsuL", "defAUlt", "dc1", "WEB"},
input: &SpiffeIDService{Host: strings.ToUpper(TestClusterID) + ".CONsuL", Namespace: "defAUlt", Datacenter: "dc1", Service: "WEB"},
want: true,
},
{
name: "service - different cluster",
id: testSigning,
input: &SpiffeIDService{"55555555-4444-3333-2222-111111111111.consul", "default", "dc1", "web"},
input: &SpiffeIDService{Host: "55555555-4444-3333-2222-111111111111.consul", Namespace: "default", Datacenter: "dc1", Service: "web"},
want: false,
},
{
name: "service - different TLD",
id: testSigning,
input: &SpiffeIDService{TestClusterID + ".fake", "default", "dc1", "web"},
input: &SpiffeIDService{Host: TestClusterID + ".fake", Namespace: "default", Datacenter: "dc1", Service: "web"},
want: false,
},
}
Expand Down
Loading

0 comments on commit ed8a901

Please sign in to comment.