Files
teleport/api/utils/keys/privatekey.go
T
Tiago SilvaandBrian Joerger f8ab3aa037 Improve tsh kube credentials read operations (#22388)
While loading a key from storage, Teleport client transverses the
storage directory to read the secrets and reads each file one by one.
This has huge impacts on `tsh kube credentials` because when calling
`kubectl` in parallel, the disk operations become overloaded and the
operation time increases exponentially.

This PR removes the transverse when reading credentials for Kubernetes
clusters and adds a quick read operation that reads the following files:
- `$TSH_HOME/current_profile`
- `$TSH_HOME/$profile.yaml`
- `$TSH_HOME/keys/$PROXY/$USER-kube/$TELEPORT_CLUSTER/$KUBE_CLUSTER-x509.pem`
- `$TSH_HOME/keys/$PROXY/$USER`

| - | P=1 | P=10 | P=40 | P=80 | P=120 |
| --- | --- | --- | --- | --- | --- |
| old direct | 9.90s user 3.14s system 64% cpu 20.284 total | 11.08s user 3.60s system 496% cpu 2.955 total | 12.46s user 4.36s system 820% cpu 2.050 total | 12.81s user 4.76s system 811% cpu 2.164 total | 13.02s user 5.07s system 861% cpu 2.100 total|
| old teleport | 18.45s user 7.80s system 51% cpu 51.085 total | 21.58s user 10.03s system 434% cpu 7.270 total | 22.33s user 10.52s system 178% cpu 18.402 total | 22.04s user 10.83s system 96% cpu 33.977 total | 22.57s user 11.45s system 55% cpu 1:01.34 total|
| new direct | 9.51s user 2.90s system 146% cpu 8.460 total | 13.75s user 4.95s system 718% cpu 2.603 total | 12.21s user 4.43s system 832% cpu 1.999 total | 11.72s user 4.50s system 839% cpu 1.933 total | 12.06s user 4.62s system 830% cpu 2.009 total |
| new teleport | 16.10s user 5.97s system 42% cpu 52.104 total | 21.62s user 9.01s system 381% cpu 8.023 total | 23.22s user 10.11s system 540% cpu 6.162 total | 21.64s user 9.81s system 603% cpu 5.208 total | 20.70s user 9.13s system 602% cpu 4.948 total |

---

| - | P=1 | P=10 | P=40 | P=80 | P=120 |
| --- | --- | --- | --- | --- | --- |
| old no tsh kube credential | 8.61s user 2.99s system 31% cpu 36.336 total | 9.57s user 3.39s system 283% cpu 4.565 total | 10.22s user 4.03s system 553% cpu 2.574 total | 9.32s user 3.82s system 559% cpu 2.351 total | 11.49s user 4.85s system 624% cpu 2.617 total
| new no tsh kube credential | 8.27s user 2.94s system 24% cpu 45.041 total | 10.85s user 4.31s system 195% cpu 7.767 total | 11.32s user 4.64s system 469% cpu 3.401 total | 10.83s user 4.51s system 553% cpu 2.773 total | 10.90s user 4.84s system 537% cpu 2.930 total |

---

| instances | old total | old avg | **new total** | **total avg** |
| --- | --- | --- | --- | --- |
| 1 | 0.145 | 0.145 | 0.074 | 0.074 |
| 10 | 0.430 | 0.043 | 0.146 | 0.0146 |
| 20 | 0.740 | 0.037 | 0.284 | 0.0142 |
| 30 | 1.108 | 0.037 | 0.417 | 0.0139 |
| 40 | 1.483 | 0.037 | 0.580 | 0.0145 |
| 60 | 13.604 | 0.228 | 0.825 | 0.0137 |
| 80 | 30.236 | 0.378 | 1.143 | 0.0142 |
| 106 | 46.681 | 0.440 | 1.413 | 0.0133 |

Fixes #19313

Co-authored-by: Brian Joerger <bjoerger@goteleport.com>
2023-03-01 19:54:36 +00:00

310 lines
9.6 KiB
Go

/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package keys defines common interfaces for Teleport client keys.
package keys
import (
"bytes"
"crypto"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"os"
"github.com/gravitational/trace"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport/api/utils/sshutils/ppk"
)
const (
PKCS1PrivateKeyType = "RSA PRIVATE KEY"
PKCS8PrivateKeyType = "PRIVATE KEY"
ECPrivateKeyType = "EC PRIVATE KEY"
pivYubiKeyPrivateKeyType = "PIV YUBIKEY PRIVATE KEY"
)
type cryptoPublicKeyI interface {
Equal(x crypto.PublicKey) bool
}
// PrivateKey implements crypto.Signer with additional helper methods. The underlying
// private key may be a standard crypto.Signer implemented in the standard library
// (aka *rsa.PrivateKey, *ecdsa.PrivateKey, or ed25519.PrivateKey), or it may be a
// custom implementation for a non-standard private key, such as a hardware key.
type PrivateKey struct {
crypto.Signer
// sshPub is the public key in ssh.PublicKey form.
sshPub ssh.PublicKey
// keyPEM is PEM-encoded private key data which can be parsed with ParsePrivateKey.
keyPEM []byte
}
// NewPrivateKey returns a new PrivateKey for the given crypto.Signer.
func NewPrivateKey(signer crypto.Signer, keyPEM []byte) (*PrivateKey, error) {
sshPub, err := ssh.NewPublicKey(signer.Public())
if err != nil {
return nil, trace.Wrap(err)
}
return &PrivateKey{
Signer: signer,
sshPub: sshPub,
keyPEM: keyPEM,
}, nil
}
// SSHPublicKey returns the ssh.PublicKey representiation of the public key.
func (k *PrivateKey) SSHPublicKey() ssh.PublicKey {
return k.sshPub
}
// SSHPublicKey returns the ssh.PublicKey representiation of the public key.
func (k *PrivateKey) MarshalSSHPublicKey() []byte {
return ssh.MarshalAuthorizedKey(k.sshPub)
}
// PrivateKeyPEM returns PEM encoded private key data. This may be data necessary
// to retrieve the key, such as a YubiKey serial number and slot, or it can be a
// PKCS marshaled private key.
//
// The resulting PEM encoded data should only be decoded with ParsePrivateKey to
// prevent errors from parsing non PKCS marshaled keys, such as a PIV key.
func (k *PrivateKey) PrivateKeyPEM() []byte {
return k.keyPEM
}
// TLSCertificate parses the given TLS certificate(s) paired with the private key
// to rerturn a tls.Certificate, ready to be used in a TLS handshake.
func (k *PrivateKey) TLSCertificate(certPEMBlock []byte) (tls.Certificate, error) {
cert := tls.Certificate{
PrivateKey: k.Signer,
}
var skippedBlockTypes []string
for {
var certDERBlock *pem.Block
certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
} else {
skippedBlockTypes = append(skippedBlockTypes, certDERBlock.Type)
}
}
if len(cert.Certificate) == 0 {
if len(skippedBlockTypes) == 0 {
return tls.Certificate{}, trace.BadParameter("tls: failed to find any PEM data in certificate input")
}
return tls.Certificate{}, trace.BadParameter("tls: failed to find \"CERTIFICATE\" PEM block in certificate input after skipping PEM blocks of the following types: %v", skippedBlockTypes)
}
// Check that the certificate's public key matches this private key.
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
if keyPub, ok := k.Public().(cryptoPublicKeyI); !ok {
return tls.Certificate{}, trace.BadParameter("private key does not contain a valid public key")
} else if !keyPub.Equal(x509Cert.PublicKey) {
return tls.Certificate{}, trace.BadParameter("private key does not match certificate's public key")
}
return cert, nil
}
// PPKFile returns a PuTTY PPK-formatted keypair
func (k *PrivateKey) PPKFile() ([]byte, error) {
rsaKey, ok := k.Signer.(*rsa.PrivateKey)
if !ok {
return nil, trace.BadParameter("cannot use private key of type %T as rsa.PrivateKey", k)
}
ppkFile, err := ppk.ConvertToPPK(rsaKey, k.MarshalSSHPublicKey())
if err != nil {
return nil, trace.Wrap(err)
}
return ppkFile, nil
}
// RSAPrivateKeyPEM returns a PEM encoded RSA private key for the given key.
// If the given key is not an RSA key, then an error will be returned.
//
// This is used by some integrations which currently only support raw RSA private keys,
// like Kubernetes, MongoDB, and PPK files for windows.
func (k *PrivateKey) RSAPrivateKeyPEM() ([]byte, error) {
if _, ok := k.Signer.(*rsa.PrivateKey); !ok {
return nil, trace.BadParameter("cannot get rsa key PEM for private key of type %T", k.Signer)
}
return k.keyPEM, nil
}
// LoadPrivateKey returns the PrivateKey for the given key file.
func LoadPrivateKey(keyFile string) (*PrivateKey, error) {
keyPEM, err := os.ReadFile(keyFile)
if err != nil {
return nil, trace.ConvertSystemError(err)
}
priv, err := ParsePrivateKey(keyPEM)
if err != nil {
return nil, trace.Wrap(err)
}
return priv, nil
}
// ParsePrivateKey returns the PrivateKey for the given key PEM block.
func ParsePrivateKey(keyPEM []byte) (*PrivateKey, error) {
block, _ := pem.Decode(keyPEM)
if block == nil {
return nil, trace.BadParameter("expected PEM encoded private key")
}
switch block.Type {
case PKCS1PrivateKeyType:
cryptoSigner, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, trace.Wrap(err)
}
return NewPrivateKey(cryptoSigner, keyPEM)
case ECPrivateKeyType:
cryptoSigner, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, trace.Wrap(err)
}
return NewPrivateKey(cryptoSigner, keyPEM)
case PKCS8PrivateKeyType:
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, trace.Wrap(err)
}
cryptoSigner, ok := priv.(crypto.Signer)
if !ok {
return nil, trace.BadParameter("x509.ParsePKCS8PrivateKey returned an invalid private key of type %T", priv)
}
return NewPrivateKey(cryptoSigner, keyPEM)
case pivYubiKeyPrivateKeyType:
priv, err := parseYubiKeyPrivateKeyData(block.Bytes)
if err != nil {
return nil, trace.Wrap(err, "failed to parse YubiKey private key")
}
return NewPrivateKey(priv, keyPEM)
default:
return nil, trace.BadParameter("unexpected private key PEM type %q", block.Type)
}
}
// LoadKeyPair returns the PrivateKey for the given private and public key files.
func LoadKeyPair(privFile, sshPubFile string) (*PrivateKey, error) {
privPEM, err := os.ReadFile(privFile)
if err != nil {
return nil, trace.ConvertSystemError(err)
}
marshalledSSHPub, err := os.ReadFile(sshPubFile)
if err != nil {
return nil, trace.ConvertSystemError(err)
}
priv, err := ParseKeyPair(privPEM, marshalledSSHPub)
if err != nil {
return nil, trace.Wrap(err)
}
return priv, nil
}
// ParseKeyPair returns the PrivateKey for the given private and public key PEM blocks.
func ParseKeyPair(privPEM, marshalledSSHPub []byte) (*PrivateKey, error) {
priv, err := ParsePrivateKey(privPEM)
if err != nil {
return nil, trace.Wrap(err)
}
// Verify that the private key's public key matches the expected public key.
if !bytes.Equal(ssh.MarshalAuthorizedKey(priv.SSHPublicKey()), marshalledSSHPub) {
return nil, trace.CompareFailed("the given private and public keys do not form a valid keypair")
}
return priv, nil
}
// LoadX509KeyPair parse a tls.Certificate from a private key file and certificate file.
// This should be used instead of tls.LoadX509KeyPair to support non-raw private keys, like PIV keys.
func LoadX509KeyPair(certFile, keyFile string) (tls.Certificate, error) {
keyPEMBlock, err := os.ReadFile(keyFile)
if err != nil {
return tls.Certificate{}, trace.ConvertSystemError(err)
}
certPEMBlock, err := os.ReadFile(certFile)
if err != nil {
return tls.Certificate{}, trace.ConvertSystemError(err)
}
tlsCert, err := X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
return tlsCert, nil
}
// X509KeyPair parse a tls.Certificate from a private key PEM and certificate PEM.
// This should be used instead of tls.X509KeyPair to support non-raw private keys, like PIV keys.
func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (tls.Certificate, error) {
priv, err := ParsePrivateKey(keyPEMBlock)
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
tlsCert, err := priv.TLSCertificate(certPEMBlock)
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
return tlsCert, nil
}
// IsRSAPrivateKey returns true if the given private key is an RSA private key.
// This function does a similar check to ParsePrivateKey, followed by key.RSAPrivateKeyPEM()
// without parsing the private fully into a crypto.Signer.
// This reduces the time it takes to check if a private key is an RSA private key
// and improves the performance compared to ParsePrivateKey by a factor of 20.
func IsRSAPrivateKey(privKey []byte) bool {
block, _ := pem.Decode(privKey)
if block == nil {
return false
}
switch block.Type {
case PKCS1PrivateKeyType:
return true
case PKCS8PrivateKeyType:
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return false
}
_, ok := priv.(*rsa.PrivateKey)
return ok
default:
return false
}
}