mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 06:08:23 +08:00
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>
310 lines
9.6 KiB
Go
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
|
|
}
|
|
}
|