Skip to content

Commit

Permalink
fixes #2165 adds network id configuration/spiffe id look up
Browse files Browse the repository at this point in the history
- adds support for trust domain lookup on x509 chain
- adds support for non-ha trustDomain configuration
- adds default generated trust domain for non-ha controllers
- non-HA controllers will generate a trust domain from the root CA if
  possible
- additionalTrustDomains has been added for transitioning between trust
  domains

update changelog.md
  • Loading branch information
andrewpmartinez committed Jul 3, 2024
1 parent 2d9910a commit 6ecca65
Show file tree
Hide file tree
Showing 24 changed files with 392 additions and 2 deletions.
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 SPIFF
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 recomended that a trust domain is speficied in configuration via URI SANs or the 'trustDomain' field

Check failure on line 28 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / codespell

recomended ==> recommended

Check failure on line 28 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / codespell

speficied ==> specified
WARNING this environment is using a default generated trust domain [spiffe://d561decf63d229d66b07de627dbbde9e93228925],
it is recomended that if network components have enrolled that the generated trust domain be added to the

Check failure on line 31 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / codespell

recomended ==> recommended
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 {
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 {
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 recomended that a trust domain is speficied in configuration via URI SANs or the 'trustDomain' field", spiffeId.String())

Check failure on line 377 in controller/config.go

View workflow job for this annotation

GitHub Actions / codespell

recomended ==> recommended

Check failure on line 377 in controller/config.go

View workflow job for this annotation

GitHub Actions / codespell

speficied ==> specified
pfxlog.Logger().Warnf("this environment is using a default generated trust domain [%s], it is recomended 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())

Check failure on line 378 in controller/config.go

View workflow job for this annotation

GitHub Actions / codespell

recomended ==> recommended
}
}

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) {
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 defualt trust domain from root CA: no root CA detected after chain assembly from the root identity server cert and ca bundle")

Check failure on line 672 in controller/config.go

View workflow job for this annotation

GitHub Actions / codespell

defualt ==> default
}

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 {
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

0 comments on commit 6ecca65

Please sign in to comment.