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

[vnet] mTLS for IPC on Windows #51856

Merged
merged 1 commit into from
Feb 10, 2025
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
22 changes: 21 additions & 1 deletion lib/vnet/admin_process_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ type windowsAdminProcessConfig struct {
// clientApplicationServiceAddr is the local TCP address of the client
// application gRPC service.
clientApplicationServiceAddr string
// serviceCredentialPath is the path where credentials for IPC with the
// client application are found.
serviceCredentialPath string
}

func (c *windowsAdminProcessConfig) check() error {
if c.clientApplicationServiceAddr == "" {
return trace.BadParameter("clientApplicationServiceAddr is required")
}
if c.serviceCredentialPath == "" {
return trace.BadParameter("serviceCredentialPath is required")
}
return nil
}

// runWindowsAdminProcess must run as administrator. It creates and sets up a TUN
Expand All @@ -43,8 +56,15 @@ type windowsAdminProcessConfig struct {
// error.
func runWindowsAdminProcess(ctx context.Context, cfg *windowsAdminProcessConfig) error {
log.InfoContext(ctx, "Running VNet admin process")
if err := cfg.check(); err != nil {
return trace.Wrap(err)
}

clt, err := newClientApplicationServiceClient(ctx, cfg.clientApplicationServiceAddr)
serviceCreds, err := readCredentials(cfg.serviceCredentialPath)
if err != nil {
return trace.Wrap(err, "reading service IPC credentials")
}
clt, err := newClientApplicationServiceClient(ctx, serviceCreds, cfg.clientApplicationServiceAddr)
if err != nil {
return trace.Wrap(err, "creating user process client")
}
Expand Down
11 changes: 7 additions & 4 deletions lib/vnet/client_application_service_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (

"github.com/gravitational/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
grpccredentials "google.golang.org/grpc/credentials"

"github.com/gravitational/teleport/api"
"github.com/gravitational/teleport/api/utils/grpc/interceptors"
Expand All @@ -36,10 +36,13 @@ type clientApplicationServiceClient struct {
conn *grpc.ClientConn
}

func newClientApplicationServiceClient(ctx context.Context, addr string) (*clientApplicationServiceClient, error) {
// TODO(nklaassen): add mTLS credentials for client application service.
func newClientApplicationServiceClient(ctx context.Context, creds *credentials, addr string) (*clientApplicationServiceClient, error) {
tlsConfig, err := creds.clientTLSConfig()
if err != nil {
return nil, trace.Wrap(err)
}
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithTransportCredentials(grpccredentials.NewTLS(tlsConfig)),
grpc.WithUnaryInterceptor(interceptors.GRPCClientUnaryErrorInterceptor),
grpc.WithStreamInterceptor(interceptors.GRPCClientStreamErrorInterceptor),
)
Expand Down
261 changes: 261 additions & 0 deletions lib/vnet/ipc_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// Teleport
// Copyright (C) 2025 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 vnet

import (
"crypto"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"os"
"path/filepath"
"time"

"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/tlsca"
)

type ipcCredentials struct {
// server holds credentials for the server-side of the connection (the VNet
// client application).
server credentials
// client holds credentials for the client-side of the connection (the VNet
// admin process).
client credentials
}

type credentials struct {
trustedCAPEM []byte // X.509 CA certificate
certPEM []byte // X.509 certificate for the VNet process
signer crypto.Signer // Private key associated with cert
}

func newIPCCredentials() (*ipcCredentials, error) {
const (
// We don't know which clusters will be connected to at this point so
// there's no way to fetch the cluster signature_algorithm_suite or unify
// suites across multiple root clusters, so just statically use ECDSA
// P-256 for these keys.
keyAlgo = cryptosuites.ECDSAP256
codingllama marked this conversation as resolved.
Show resolved Hide resolved
// These certs need to be valid for the full VNet process lifetime,
// which could be longer than any individual Teleport session. Going
// with 30 days for now, which should be more than long enough.
certTTL = 30 * 24 * time.Hour
certOrganizationalUnit = "TeleportVNet"
)

serverCASigner, err := cryptosuites.GenerateKeyWithAlgorithm(keyAlgo)
if err != nil {
return nil, trace.Wrap(err, "generating server CA key")
}
clientCASigner, err := cryptosuites.GenerateKeyWithAlgorithm(keyAlgo)
if err != nil {
return nil, trace.Wrap(err, "generating client CA key")
}
serverSigner, err := cryptosuites.GenerateKeyWithAlgorithm(keyAlgo)
if err != nil {
return nil, trace.Wrap(err, "generating server key")
}
clientSigner, err := cryptosuites.GenerateKeyWithAlgorithm(keyAlgo)
if err != nil {
return nil, trace.Wrap(err, "generating client key")
}

serverCAPEM, err := tlsca.GenerateSelfSignedCAWithSigner(
serverCASigner,
pkix.Name{
OrganizationalUnit: []string{certOrganizationalUnit},
CommonName: "Server CA",
},
nil, // dnsNames
certTTL,
)
if err != nil {
return nil, trace.Wrap(err, "generating self-signed server CA")
}
serverCA, err := tlsca.FromCertAndSigner(serverCAPEM, serverCASigner)
if err != nil {
return nil, trace.Wrap(err)
}
clientCAPEM, err := tlsca.GenerateSelfSignedCAWithSigner(
clientCASigner,
pkix.Name{
OrganizationalUnit: []string{certOrganizationalUnit},
CommonName: "Client CA",
},
nil, // dnsNames
certTTL,
)
if err != nil {
return nil, trace.Wrap(err, "generating self-signed client CA")
}
clientCA, err := tlsca.FromCertAndSigner(clientCAPEM, clientCASigner)
if err != nil {
return nil, trace.Wrap(err)
}

now := time.Now()
serverCertPEM, err := serverCA.GenerateCertificate(tlsca.CertificateRequest{
PublicKey: serverSigner.Public(),
Subject: pkix.Name{
OrganizationalUnit: []string{certOrganizationalUnit},
CommonName: "localhost",
},
DNSNames: []string{"localhost", "127.0.0.1", "::1"},
NotAfter: now.Add(certTTL),
})
if err != nil {
return nil, trace.Wrap(err, "generating server TLS certificate")
}
clientCertPEM, err := clientCA.GenerateCertificate(tlsca.CertificateRequest{
PublicKey: clientSigner.Public(),
Subject: pkix.Name{
OrganizationalUnit: []string{certOrganizationalUnit},
CommonName: "client",
},
NotAfter: now.Add(certTTL),
})
if err != nil {
return nil, trace.Wrap(err, "generating client TLS certificate")
}

return &ipcCredentials{
server: credentials{
trustedCAPEM: clientCAPEM,
certPEM: serverCertPEM,
signer: serverSigner,
},
client: credentials{
trustedCAPEM: serverCAPEM,
certPEM: clientCertPEM,
signer: clientSigner,
},
}, nil
}

func (c *credentials) serverTLSConfig() (*tls.Config, error) {
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(c.trustedCAPEM) {
return nil, trace.Errorf("parsing trusted CA certificate")
}

tlsCert, err := keys.TLSCertificateForSigner(c.signer, c.certPEM)
if err != nil {
return nil, trace.Wrap(err, "parsing VNet server certificate")
}

return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
ClientCAs: caPool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
}, nil
}

func (c *credentials) clientTLSConfig() (*tls.Config, error) {
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(c.trustedCAPEM) {
return nil, trace.Errorf("parsing trusted CA certificate")
}

tlsCert, err := keys.TLSCertificateForSigner(c.signer, c.certPEM)
if err != nil {
return nil, trace.Wrap(err, "parsing VNet client certificate")
}

return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
RootCAs: caPool,
MinVersion: tls.VersionTLS13,
}, nil
}

const (
caFileName = "ca.pem"
certFileName = "cert.pem"
keyFileName = "key.pem"
)

// write writes the credentials to the filesystem directory.
func (c *credentials) write(dir string) (err error) {
// Attempt to clean up if returning an error for any reason.
defer func() {
if err == nil {
return
}
deleteErr := trace.Wrap(c.remove(dir), "cleaning up after failing to write credentials")
err = trace.NewAggregate(err, deleteErr)
}()
keyPEM, err := keys.MarshalPrivateKey(c.signer)
if err != nil {
return trace.Wrap(err)
}
for fileName, data := range map[string][]byte{
caFileName: c.trustedCAPEM,
certFileName: c.certPEM,
keyFileName: keyPEM,
} {
filePath := filepath.Join(dir, fileName)
if err := os.WriteFile(filePath, data, 0600); err != nil {
return trace.Wrap(err, "writing service credential file %s", filePath)
}
}
return nil
}

// remove removes the files from the filesystem directory.
// Note: can't just call os.RemoveAll in case the current user does not have
// permissions to list files.
func (c *credentials) remove(dir string) error {
var errors []error
for _, fileName := range []string{
caFileName, certFileName, keyFileName,
} {
filePath := filepath.Join(dir, fileName)
if err := os.Remove(filePath); err != nil {
errors = append(errors, trace.Wrap(err, "deleting service credential file %s", filePath))
}
}
return trace.NewAggregate(errors...)
}

func readCredentials(dir string) (*credentials, error) {
caBytes, err := os.ReadFile(filepath.Join(dir, caFileName))
if err != nil {
return nil, trace.Wrap(err, "reading service credential file %s", caFileName)
}
certBytes, err := os.ReadFile(filepath.Join(dir, certFileName))
if err != nil {
return nil, trace.Wrap(err, "reading service credential file %s", certFileName)
}
keyBytes, err := os.ReadFile(filepath.Join(dir, keyFileName))
if err != nil {
return nil, trace.Wrap(err, "reading service credential file %s", keyFileName)
}
signer, err := keys.ParsePrivateKey(keyBytes)
if err != nil {
return nil, trace.Wrap(err, "parsing service private key")
}
codingllama marked this conversation as resolved.
Show resolved Hide resolved
return &credentials{
trustedCAPEM: caBytes,
certPEM: certBytes,
signer: signer,
}, nil
}
12 changes: 10 additions & 2 deletions lib/vnet/service_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ func startService(ctx context.Context, cfg *windowsAdminProcessConfig) (*mgr.Ser
Name: serviceName,
Handle: serviceHandle,
}
if err := service.Start(ServiceCommand, "--addr", cfg.clientApplicationServiceAddr); err != nil {
if err := service.Start(ServiceCommand,
"--addr", cfg.clientApplicationServiceAddr,
"--cred-path", cfg.serviceCredentialPath,
); err != nil {
return nil, trace.Wrap(err, "starting Windows service %s", serviceName)
}
return service, nil
Expand Down Expand Up @@ -158,10 +161,14 @@ loop:
}

func (s *windowsService) run(ctx context.Context, args []string) error {
var clientApplicationServiceAddr string
var (
clientApplicationServiceAddr string
credPath string
)
app := kingpin.New(serviceName, "Teleport VNet Windows Service")
serviceCmd := app.Command("vnet-service", "Start the VNet service.")
serviceCmd.Flag("addr", "client application service address").Required().StringVar(&clientApplicationServiceAddr)
serviceCmd.Flag("cred-path", "path to TLS credentials for connecting to client application").Required().StringVar(&credPath)
cmd, err := app.Parse(args[1:])
if err != nil {
return trace.Wrap(err, "parsing runtime arguments to Windows service")
Expand All @@ -171,6 +178,7 @@ func (s *windowsService) run(ctx context.Context, args []string) error {
}
if err := runWindowsAdminProcess(ctx, &windowsAdminProcessConfig{
clientApplicationServiceAddr: clientApplicationServiceAddr,
serviceCredentialPath: credPath,
}); err != nil {
return trace.Wrap(err, "running admin process")
}
Expand Down
Loading
Loading