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

Workload Identity: Federation bundle endpoint #44870

Merged
merged 12 commits into from
Aug 2, 2024
3 changes: 3 additions & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,9 @@ func (h *Handler) bindDefaultEndpoints() {
h.GET(OIDCJWKWURI, h.WithLimiter(h.jwksOIDC))
h.GET("/webapi/thumbprint", h.WithLimiter(h.thumbprint))

// SPIFFE Federation Trust Bundle
h.GET("/webapi/spiffe/bundle.json", h.WithLimiter(h.getSPIFFEBundle))

// DiscoveryConfig CRUD
h.GET("/webapi/sites/:site/discoveryconfig", h.WithClusterAuth(h.discoveryconfigList))
h.POST("/webapi/sites/:site/discoveryconfig", h.WithClusterAuth(h.discoveryconfigCreate))
Expand Down
93 changes: 93 additions & 0 deletions lib/web/spiffe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Teleport
// Copyright (C) 2024 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package web

import (
"net/http"
"time"

"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
"github.com/spiffe/go-spiffe/v2/bundle/spiffebundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
)

// getSPIFFEBundle returns the SPIFFE-compatible trust bundle which allows other
// trust domains to federate with this Teleport cluster.
//
// Mounted at /webapi/spiffe/bundle.json
//
// Must abide by the standard for a "https_web" profile as described in
// https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md#5-serving-and-consuming-a-spiffe-bundle-endpoint
func (h *Handler) getSPIFFEBundle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) (any, error) {
cn, err := h.GetAccessPoint().GetClusterName()
if err != nil {
return nil, trace.Wrap(err, "fetching cluster name")
}

td, err := spiffeid.TrustDomainFromString(cn.GetClusterName())
if err != nil {
return nil, trace.Wrap(err, "creating trust domain")
}

bundle := spiffebundle.New(td)
// The refresh hint indicates how often a federated trust domain should
// check for updates to the bundle. This should be a low value to ensure
// that CA rotations are picked up quickly. Since we're leveraging
// https_web, it's not critical for a federated trust domain to catch
// all phases of the rotation - however, if we support https_spiffe in
// future, we may need to consider a lower value or enforcing a wait
// period during rotations equivalent to the refresh hint.
bundle.SetRefreshHint(5 * time.Minute)
// TODO(noah):
// For now, we omit the SequenceNumber field. This is only a SHOULD not a
// MUST per the spec. To add this, we will add a sequence number to the
// cert authority and increment it on every update.

const loadKeysFalse = false
spiffeCA, err := h.GetAccessPoint().GetCertAuthority(r.Context(), types.CertAuthID{
Type: types.SPIFFECA,
DomainName: cn.GetClusterName(),
}, loadKeysFalse)
if err != nil {
return nil, trace.Wrap(err, "fetching SPIFFE CA")
}

for _, certPEM := range services.GetTLSCerts(spiffeCA) {
cert, err := tlsca.ParseCertificatePEM(certPEM)
if err != nil {
return nil, trace.Wrap(err, "parsing certificate")
}
bundle.AddX509Authority(cert)
}

bundleBytes, err := bundle.Marshal()
if err != nil {
return nil, trace.Wrap(err, "marshaling bundle")
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err = w.Write(bundleBytes); err != nil {
h.logger.DebugContext(h.cfg.Context, "Failed to write SPIFFE bundle response", "error", err)
}
return nil, nil
}
72 changes: 72 additions & 0 deletions lib/web/spiffe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package web

import (
"context"
"crypto/x509"
"testing"

"github.com/gravitational/roundtrip"
"github.com/spiffe/go-spiffe/v2/bundle/spiffebundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
)

func TestGetSPIFFEBundle(t *testing.T) {
ctx := context.Background()
env := newWebPack(t, 1)
authServer := env.server.Auth()
cn, err := authServer.GetClusterName()
require.NoError(t, err)
ca, err := authServer.GetCertAuthority(ctx, types.CertAuthID{
Type: types.SPIFFECA,
DomainName: cn.GetClusterName(),
}, false)
require.NoError(t, err)

var wantCACerts []*x509.Certificate
for _, certPem := range services.GetTLSCerts(ca) {
cert, err := tlsca.ParseCertificatePEM(certPem)
require.NoError(t, err)
wantCACerts = append(wantCACerts, cert)
}

clt, err := client.NewWebClient(env.proxies[0].webURL.String(), roundtrip.HTTPClient(client.NewInsecureWebClient()))
require.NoError(t, err)

res, err := clt.Get(ctx, clt.Endpoint("webapi", "spiffe", "bundle.json"), nil)
require.NoError(t, err)

td, err := spiffeid.TrustDomainFromString(cn.GetClusterName())
require.NoError(t, err)
gotBundle, err := spiffebundle.Read(td, res.Reader())
require.NoError(t, err)

require.Len(t, gotBundle.X509Authorities(), len(wantCACerts))
for _, caCert := range wantCACerts {
require.True(t, gotBundle.HasX509Authority(caCert), "certificate not found in bundle")
}
}
Loading