Skip to content
Closed
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
126 changes: 126 additions & 0 deletions examples/go-client-with-refresh/dynamic/dynamic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package dynamic

import (
"crypto/tls"
"crypto/x509"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/identityfile"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"
"golang.org/x/net/http2"
"sync"
)

type DynamicIdentityFile struct {
mu sync.RWMutex
tlsCert *tls.Certificate
pool *x509.CertPool

path string
clusterName string
}

func NewDynamicIdentityFile(path string, clusterName string) (*DynamicIdentityFile, error) {
d := &DynamicIdentityFile{
path: path,
clusterName: clusterName,
}

err := d.Reload()
if err != nil {
return nil, trace.Wrap(err)
}

return d, nil
}

func (d *DynamicIdentityFile) Reload() error {
id, err := identityfile.ReadFile(d.path)
if err != nil {
return trace.Wrap(err)
}

cert, err := keys.X509KeyPair(id.Certs.TLS, id.PrivateKey)
if err != nil {
return trace.Wrap(err)
}

pool := x509.NewCertPool()
for _, caCerts := range id.CACerts.TLS {
if !pool.AppendCertsFromPEM(caCerts) {
return trace.BadParameter("invalid CA cert PEM")
}
}

d.mu.Lock()
d.pool = pool
d.tlsCert = &cert
d.mu.Unlock()
return nil
}

func (d *DynamicIdentityFile) Dialer(
cfg client.Config,
) (client.ContextDialer, error) {
// Returning a dialer isn't necessary for this credential.
return nil, trace.NotImplemented("no dialer")
}

func (d *DynamicIdentityFile) TLSConfig() (*tls.Config, error) {
cfg := &tls.Config{
// GetClientCertificate is used instead of the static Certificates
// field.
Certificates: nil,
// Encoded cluster name required to ensure requests are routed to the
// correct cloud tenants.
ServerName: utils.EncodeClusterName(d.clusterName),
GetClientCertificate: func(
info *tls.CertificateRequestInfo,
) (*tls.Certificate, error) {
// GetClientCertificate callback is used to allow us to dynamically
// change the certificate when reloaded.
d.mu.RLock()
defer d.mu.RUnlock()
return d.tlsCert, nil
},
// InsecureSkipVerify is forced true to ensure that only our
// VerifyConnection callback is used to verify the server's presented
// certificate.
InsecureSkipVerify: true,
VerifyConnection: func(state tls.ConnectionState) error {
// This VerifyConnection callback is based on the standard library
// implementation of verifyServerCertificate in the tls package.
// We provide our own implementation so we can dynamically handle
// a changing CA Roots pool.
d.mu.RLock()
defer d.mu.RUnlock()

opts := x509.VerifyOptions{
DNSName: state.ServerName,
Intermediates: x509.NewCertPool(),
Roots: d.pool,
}
for _, cert := range state.PeerCertificates[1:] {
// Whilst we don't currently use intermediate certs at
// Teleport, including this here means that we are
// future-proofed in case we do.
opts.Intermediates.AddCert(cert)
}
_, err := state.PeerCertificates[0].Verify(opts)
return err
},
NextProtos: []string{http2.NextProtoTLS},
}

return cfg, nil
}

func (d *DynamicIdentityFile) SSHClientConfig() (*ssh.ClientConfig, error) {
// For now, SSH Client Config is disabled until I can wrap my head around
// the changes needed to make an SSH config dynamic.
// This means the auth server must be available directly or using
// the ALPN/SNI.
return nil, trace.NotImplemented("no ssh config")
}
104 changes: 104 additions & 0 deletions examples/go-client-with-refresh/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"context"
"fmt"
"github.com/gravitational/teleport/api/client"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/examples/go-client-with-refresh/dynamic"
teleUtils "github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"google.golang.org/grpc"
"os"
"os/signal"
"time"
)

func main() {
ctx, cancel := signal.NotifyContext(
context.Background(),
unix.SIGTERM,
unix.SIGINT,
)
defer cancel()

log := teleUtils.NewLogger()
if err := run(ctx, log); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func run(ctx context.Context, log logrus.FieldLogger) error {
proxyAddr := os.Getenv("PROXY_ADDR") // e.g noah.teleport.sh:443
identityFilePath := os.Getenv("TELEPORT_IDENTITY_FILE") // e.g ./identity-file
clusterName := os.Getenv("CLUSTER_NAME") // e.g noah.teleport.sh

cred, err := dynamic.NewDynamicIdentityFile(identityFilePath, clusterName)
go func() {
// This goroutine loop could be replaced with a file watcher.
for {
time.Sleep(time.Second * 30)
if err := cred.Reload(); err != nil {
log.WithError(err).Warn("Failed to reload identity file")
continue
}
log.Info("Successfully reloaded identity file from disk. New client connections will use this identity.")
}
}()

cfg := client.Config{
Addrs: []string{proxyAddr},
Credentials: []client.Credentials{
cred,
},
DialOpts: []grpc.DialOption{
// Provides better feedback on connection errors
grpc.WithReturnConnectionError(),
},
// ALPNSNIAuthDialClusterName allows the client to connect to the
// auth server through the proxy.
ALPNSNIAuthDialClusterName: clusterName,
}
clt, err := client.New(ctx, cfg)
if err != nil {
return trace.Wrap(err)
}
defer clt.Close()

return monitorLoop(ctx, log, clt)
}

// This loop replicates some work that needs to run continously against the
// Teleport API and have access to an up to date client.
func monitorLoop(
ctx context.Context,
log logrus.FieldLogger,
clt *client.Client,
) error {
for {
// Exit is context is cancelled.
if err := ctx.Err(); err != nil {
log.Info(
"Detected context cancellation, exiting watch loop!",
)
return nil
}

// This action represents any unary action against the Teleport API
start := time.Now()
nodes, err := clt.GetNodes(ctx, apidefaults.Namespace)
if err != nil {
log.WithError(err).Error("failed to fetch nodes")
} else {
log.WithFields(logrus.Fields{
"count": len(nodes),
"duration": time.Since(start),
}).Info("Fetched nodes list")
}

time.Sleep(5 * time.Second)
}
}