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

fixes #2165 adds trust domain (network id) configuration/spiffe id look up #2189

Merged
merged 4 commits into from
Jul 4, 2024
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
57 changes: 56 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,58 @@
# Release 1.1.6

## What's New

* Trust Domain Configuration

## Trust Domain Configuration

OpenZiti controllers from this release forward will now require a `trust domain` to be configured.
High Availability (HA) controllers already have this requirement. HA Controllers configure their trust domain via SPIFFE
ids that are embedded in x509 certificates.

For feature parity, non-HA controllers will now have this same requirement. However, as re-issuing certificates is not
always easily done. To help with the transition, non-HA controllers will have the ability to have their trust domain
sourced from the controller configuration file through the root configuration value `trustDomain`. The configuration
field which takes a string that must be URI hostname compatible (see: https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md).
If this value is not defined, a trust domain will be generated from the root CA certificate of the controller.

For networks that will be deployed after this change, it is highly suggested that a SPIFFE id is added to certificates.
The `ziti pki create ...` tooling supports the `--spiffe-id` option to help handle this scenario.

### Generated Trust Domain Log Messages

The following log messages are examples of warnings produced when a controller is using a generated trust domain:

```
WARNING this environment is using a default generated trust domain [spiffe://d561decf63d229d66b07de627dbbde9e93228925],
it is recommended that a trust domain is specified in configuration via URI SANs or the 'trustDomain' field

WARNING this environment is using a default generated trust domain [spiffe://d561decf63d229d66b07de627dbbde9e93228925],
it is recommended that if network components have enrolled that the generated trust domain be added to the
configuration field 'additionalTrustDomains'
```

### Trust domain resolution:

- Non-HA controllers
- Prefers SPIFFE ids in x509 certificate URI SANs, looking at the leaf up the signing chain
- Regresses to `trustDomain` in the controller configuration file if not found
- Regress to generating a trust domain from the server certificates root CA, if the above do not resolve

- HA Controllers
- Requires x509 SPIFFE ids in x509 certificate URI SANs

### Additional Trust Domains

When moving between trust domains (i.e. from the default generated to a new named one), the controller supports having
other trust domains. The trust domains do not replace certificate chain validation, which is still checked and enforced.

Additional trust domains are configured in the controller configuration file under the root field
`additionalTrustDomains`. This field is an array of hostname safe strings.

The most common use case for this is field is if a network has issued certificates using the generated trust domain and
now wants to transition to a explicitly defined one.

# Release 1.1.5

## What's New
Expand All @@ -24,8 +79,8 @@

## What's New

* Bug fixes
* Controller HA Beta 1
* Bug fixes

## Controller HA Beta 1

Expand Down
257 changes: 256 additions & 1 deletion controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controller

import (
"bytes"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"fmt"
Expand All @@ -39,6 +40,7 @@ import (
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"math"
"net/url"
"os"
"strings"
"time"
Expand Down Expand Up @@ -77,7 +79,10 @@ const (
)

type Config struct {
Id *identity.TokenId
Id *identity.TokenId
SpiffeIdTrustDomain *url.URL
AdditionalTrustDomains []*url.URL

Raft *raft.Config
Network *network.Options
Db boltz.Db
Expand Down Expand Up @@ -313,6 +318,107 @@ func LoadConfig(path string) (*Config, error) {
panic("controllerConfig must provide [db] or [raft]")
}

//SPIFFE Trust Domain
var spiffeId *url.URL
if controllerConfig.Raft != nil {
//HA setup, SPIFFE ID must come from certs
var err error
spiffeId, err = GetSpiffeIdFromIdentity(controllerConfig.Id.Identity)
if err != nil {
panic("error determining a trust domain from a SPIFFE id in the root identity for HA configuration, must have a spiffe:// URI SANs in the server certificate or along the signing CAs chain: " + err.Error())
}

if spiffeId == nil {
panic("unable to determine a trust domain from a SPIFFE id in the root identity for HA configuration, must have a spiffe:// URI SANs in the server certificate or along the signing CAs chain")
}
} else {
// Non-HA/legacy system, prefer SPIFFE id from certs, but fall back to configuration if necessary
spiffeId, _ = GetSpiffeIdFromIdentity(controllerConfig.Id.Identity)

if spiffeId == nil {
//for non HA setups allow the trust domain to come from the configuration root value `trustDomain`
if value, found := cfgmap["trustDomain"]; found {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it really would be swell to get important constants like "trustDomain" documented and into a constants type location...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a similar thought; I followed the pattern in the file. I am not going to do it in this PR. If someone actually needs those constants they can go through and do it.

trustDomain, ok := value.(string)

if !ok {
panic(fmt.Sprintf("could not parse [trustDomain], expected a string got [%T]", value))
}

if trustDomain != "" {
if !strings.HasPrefix("spiffe://", trustDomain) {
trustDomain = "spiffe://" + trustDomain
}

spiffeId, err = url.Parse(trustDomain)

if err != nil {
andrewpmartinez marked this conversation as resolved.
Show resolved Hide resolved
panic("could not parse [trustDomain] when used in a SPIFFE id URI [" + trustDomain + "], please make sure it is a valid URI hostname: " + err.Error())
}

if spiffeId == nil {
panic("could not parse [trustDomain] when used in a SPIFFE id URI [" + trustDomain + "]: spiffeId is nil and no error returned")
}

if spiffeId.Scheme != "spiffe" {
panic("[trustDomain] does not have a spiffe scheme (spiffe://) has: " + spiffeId.Scheme)
}
}
}
}

//default a generated trust domain and spiffe id from the sha1 of the root ca
if spiffeId == nil {
spiffeId, err = generateDefaultSpiffeId(controllerConfig.Id.Identity)

if err != nil {
panic("could not generate default trust domain: " + err.Error())
}

pfxlog.Logger().Warnf("this environment is using a default generated trust domain [%s], it is recommended that a trust domain is specified in configuration via URI SANs or the 'trustDomain' field", spiffeId.String())
pfxlog.Logger().Warnf("this environment is using a default generated trust domain [%s], it is recommended that if network components have enrolled that the generated trust domain be added to the configuration field 'additionalTrustDomains' array when configuring a explicit trust domain", spiffeId.String())
}
}

if spiffeId == nil {
panic("unable to determine trust domain from SPIFFE id (spiffe:// URI SANs in server cert or signing CAs) or from configuration [trustDomain], controllers must have a trust domain")
}

if spiffeId.Hostname() == "" {
panic("unable to determine trust domain from SPIFFE id: hostname was empty")
}

//only preserve trust domain
spiffeId.Path = ""
controllerConfig.SpiffeIdTrustDomain = spiffeId

if value, found := cfgmap["additionalTrustDomains"]; found {
if valArr, ok := value.([]any); ok {
var trustDomains []*url.URL
for _, trustDomain := range valArr {
if strTrustDomain, ok := trustDomain.(string); ok {

if !strings.HasPrefix("spiffe://", strTrustDomain) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is spiffee:// case sensitive?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the scheme and hostname are both case-insensitive, but I will test them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheme is lowercase during parsing. The hostname is not.

strTrustDomain = "spiffe://" + strTrustDomain
}

spiffeId, err = url.Parse(strTrustDomain)

if err != nil {
panic(fmt.Sprintf("invalid entry in 'additionalTrustDomains', could not be parsed as a URI: %v", trustDomain))
}
//only preserve trust domain
spiffeId.Path = ""

trustDomains = append(trustDomains, spiffeId)
} else {
panic(fmt.Sprintf("invalid entry in 'additionalTrustDomains' expected a string: %v", trustDomain))
}
}

controllerConfig.AdditionalTrustDomains = trustDomains
}
}

if value, found := cfgmap["trace"]; found {
if submap, ok := value.(map[interface{}]interface{}); ok {
if value, found := submap["path"]; found {
Expand Down Expand Up @@ -542,6 +648,155 @@ func LoadConfig(path string) (*Config, error) {
return controllerConfig, nil
}

// isSelfSigned checks if the given certificate is self-signed.
func isSelfSigned(cert *x509.Certificate) (bool, error) {
// Check if the Issuer and Subject fields are equal
if cert.Issuer.String() != cert.Subject.String() {
return false, nil
}

// Attempt to verify the certificate's signature with its own public key
err := cert.CheckSignatureFrom(cert)
if err != nil {
return false, err
}

return true, nil
}

func generateDefaultSpiffeId(id identity.Identity) (*url.URL, error) {
chain := id.CaPool().GetChain(id.Cert().Leaf)

// chain is 0 or 1, no root possible
if len(chain) <= 1 {
return nil, fmt.Errorf("error generating default trust domain from root CA: no root CA detected after chain assembly from the root identity server cert and ca bundle")
}

candidateRoot := chain[len(chain)-1]

if candidateRoot == nil {
return nil, fmt.Errorf("encountered nil candidate root ca during default trust domain generation")
}

if !candidateRoot.IsCA {
return nil, fmt.Errorf("candidate root CA is not flagged with the x509 CA flag")
}

if selfSigned, _ := isSelfSigned(candidateRoot); !selfSigned {
return nil, errors.New("candidate root CA is not self signed")
}

rawHash := sha1.Sum(candidateRoot.Raw)

fingerprint := fmt.Sprintf("%x", rawHash)
idStr := "spiffe://" + fingerprint

spiffeId, err := url.Parse(idStr)

if err != nil {
return nil, fmt.Errorf("could not parse generated SPIFFE id [%s] as a URI: %w", idStr, err)
}

return spiffeId, nil
}

// GetSpiffeIdFromIdentity will search an Identity for a trust domain encoded as a spiffe:// URI SAN starting
// from the server cert and up its signing chain. Each certificate must contain 0 or 1 spiffe:// URI SAN. The first
// SPIFFE id looking up the chain back to the root CA is returned. If no SPIFFE id is encountered, nil is returned.
// Errors are returned for parsing and processing errors only.
func GetSpiffeIdFromIdentity(id identity.Identity) (*url.URL, error) {
tlsCerts := id.ServerCert()

spiffeId, err := GetSpiffeIdFromTlsCertChain(tlsCerts)

if err != nil {
return nil, fmt.Errorf("failed to acquire SPIFFE id from server certs: %w", err)
}

if spiffeId != nil {
return spiffeId, nil
}

if len(tlsCerts) > 0 {
chain := id.CaPool().GetChain(tlsCerts[0].Leaf)

if len(chain) > 0 {
spiffeId, _ = GetSpiffeIdFromCertChain(chain)
}
}

if spiffeId == nil {
return nil, errors.Errorf("SPIFFE id not found in identity")
}

return spiffeId, nil
}

// GetSpiffeIdFromCertChain cycles through a slice of certificates that goes from leaf up CAs. Each certificate
// must contain 0 or 1 spiffe:// URI SAN. The first encountered SPIFFE id looking up the chain back to the root CA is returned.
// If no SPIFFE id is encountered, nil is returned. Errors are returned for parsing and processing errors only.
func GetSpiffeIdFromCertChain(certs []*x509.Certificate) (*url.URL, error) {
var spiffeId *url.URL
for _, cert := range certs {
var err error
spiffeId, err = GetSpiffeIdFromCert(cert)

if err != nil {
return nil, fmt.Errorf("failed to determine SPIFFE ID from x509 certificate chain: %w", err)
}

if spiffeId != nil {
return spiffeId, nil
}
}

return nil, errors.New("failed to determine SPIFFE ID, no spiffe:// URI SANs found in x509 certificate chain")
}

// GetSpiffeIdFromTlsCertChain will search a tls certificate chain for a trust domain encoded as a spiffe:// URI SAN.
// Each certificate must contain 0 or 1 spiffe:// URI SAN. The first SPIFFE id looking up the chain is returned. If
// no SPIFFE id is encountered, nil is returned. Errors are returned for parsing and processing errors only.
func GetSpiffeIdFromTlsCertChain(tlsCerts []*tls.Certificate) (*url.URL, error) {
for _, tlsCert := range tlsCerts {
for i, rawCert := range tlsCert.Certificate {
cert, err := x509.ParseCertificate(rawCert)

if err != nil {
return nil, fmt.Errorf("failed to parse TLS cert at index [%d]: %w", i, err)
}

spiffeId, err := GetSpiffeIdFromCert(cert)

if err != nil {
return nil, fmt.Errorf("failed to determine SPIFFE ID from TLS cert at index [%d]: %w", i, err)
}

if spiffeId != nil {
andrewpmartinez marked this conversation as resolved.
Show resolved Hide resolved
return spiffeId, nil
}
}
}

return nil, nil
}

// GetSpiffeIdFromCert will search a x509 certificate for a trust domain encoded as a spiffe:// URI SAN.
// Each certificate must contain 0 or 1 spiffe:// URI SAN. The first SPIFFE id looking up the chain is returned. If
// no SPIFFE id is encountered, nil is returned. Errors are returned for parsing and processing errors only.
func GetSpiffeIdFromCert(cert *x509.Certificate) (*url.URL, error) {
var spiffeId *url.URL
for _, uriSan := range cert.URIs {
if uriSan.Scheme == "spiffe" {
if spiffeId != nil {
return nil, fmt.Errorf("multiple URI SAN spiffe:// ids encountered, must only have one, encountered at least two: [%s] and [%s]", spiffeId.String(), uriSan.String())
}
spiffeId = uriSan
}
}

return spiffeId, nil
}

func loadTlsHandshakeRateLimiterConfig(rateLimitConfig *command.AdaptiveRateLimiterConfig, cfgmap map[interface{}]interface{}) error {
if value, found := cfgmap["rateLimiter"]; found {
if submap, ok := value.(map[interface{}]interface{}); ok {
Expand Down
4 changes: 4 additions & 0 deletions controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ type Controller struct {
apiDataOnce sync.Once
}

func (c *Controller) GetConfig() *Config {
return c.config
}

func (c *Controller) GetPeerSigners() []*x509.Certificate {
if c.raftController == nil || c.raftController.Mesh == nil {
return nil
Expand Down
3 changes: 3 additions & 0 deletions controller/env/appenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
"github.com/openziti/ziti/common"
"github.com/openziti/ziti/common/cert"
"github.com/openziti/ziti/common/eid"
"github.com/openziti/ziti/controller"
"github.com/openziti/ziti/controller/api"
"github.com/openziti/ziti/controller/command"
"github.com/openziti/ziti/controller/config"
Expand Down Expand Up @@ -311,6 +312,7 @@ type HostController interface {
GetPeerAddresses() []string
GetRaftInfo() (string, string, string)
GetApiAddresses() (map[string][]event.ApiAddress, []byte)
GetConfig() *controller.Config
}

type Schemes struct {
Expand Down Expand Up @@ -557,6 +559,7 @@ func (ae *AppEnv) FillRequestContext(rc *response.RequestContext) error {
token := ae.getJwtTokenFromRequest(rc.Request)

if token != nil {
rc.IsJwtToken = true
return ae.ProcessJwt(rc, token)
}

Expand Down
Loading
Loading