Files
Gabriel Corado e5c992ccc1 App service use upstream TLS options (#66454)
* feat: app service use upstream tls options

* refactor: code review suggestions

* refactor(upstreamtls): code review suggestions

* test(app): provide missing transport options

* chore(app): fix lint
2026-05-27 13:41:28 +00:00

380 lines
11 KiB
Go

/*
* Teleport
* Copyright (C) 2023 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 services
import (
"crypto"
"crypto/tls"
"crypto/x509"
"iter"
"slices"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/gravitational/teleport/api/types"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/jwt"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
// ValidateCertAuthority validates the CertAuthority
func ValidateCertAuthority(ca types.CertAuthority) (err error) {
if err = CheckAndSetDefaults(ca); err != nil {
return trace.Wrap(err)
}
switch ca.GetType() {
case types.UserCA, types.HostCA:
err = checkUserOrHostCA(ca)
case types.DatabaseCA, types.DatabaseClientCA:
err = checkDatabaseCA(ca)
case types.OpenSSHCA:
err = checkOpenSSHCA(ca)
case types.JWTSigner, types.OIDCIdPCA, types.OktaCA, types.BoundKeypairCA:
err = checkJWTKeys(ca)
case types.SAMLIDPCA:
err = checkSAMLIDPCA(ca)
case types.SPIFFECA:
err = checkSPIFFECA(ca)
case types.AWSRACA:
err = checkAWSRACA(ca)
case types.WindowsCA:
err = checkWindowsCA(ca)
case types.AppClientCA:
err = checkAppClientCA(ca)
default:
return trace.BadParameter("invalid CA type %q", ca.GetType())
}
return trace.Wrap(err)
}
func checkSPIFFECA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
if len(ca.Spec.ActiveKeys.TLS) == 0 {
return trace.BadParameter("certificate authority missing TLS key pairs")
}
if len(ca.Spec.ActiveKeys.JWT) == 0 {
return trace.BadParameter("certificate authority missing JWT key pairs")
}
return nil
}
func checkAWSRACA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
if len(ca.Spec.ActiveKeys.TLS) == 0 {
return trace.BadParameter("certificate authority missing TLS key pairs")
}
return nil
}
func checkUserOrHostCA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
if len(ca.Spec.ActiveKeys.SSH) == 0 {
return trace.BadParameter("certificate authority missing SSH key pairs")
}
if len(ca.Spec.ActiveKeys.TLS) == 0 {
return trace.BadParameter("certificate authority missing TLS key pairs")
}
if _, err := sshutils.GetCheckers(ca); err != nil {
return trace.Wrap(err)
}
if err := sshutils.ValidateSigners(ca); err != nil {
return trace.Wrap(err)
}
// This is to force users to migrate
if len(ca.GetRoles()) != 0 && len(ca.GetRoleMap()) != 0 {
return trace.BadParameter("should set either 'roles' or 'role_map', not both")
}
_, err := parseRoleMap(ca.GetRoleMap())
return trace.Wrap(err)
}
// checkDatabaseCA checks if provided certificate authority contains a valid TLS key pair.
// This function is used to verify Database CA.
func checkDatabaseCA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
return trace.Wrap(checkTLSKeys(ca))
}
// checkOpenSSHCA checks if provided certificate authority contains a valid SSH key pair.
func checkOpenSSHCA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
if len(ca.Spec.ActiveKeys.SSH) == 0 {
return trace.BadParameter("certificate authority missing SSH key pairs")
}
if _, err := sshutils.GetCheckers(ca); err != nil {
return trace.Wrap(err)
}
if err := sshutils.ValidateSigners(ca); err != nil {
return trace.Wrap(err)
}
// This is to force users to migrate
if len(ca.GetRoles()) != 0 && len(ca.GetRoleMap()) != 0 {
return trace.BadParameter("should set either 'roles' or 'role_map', not both")
}
_, err := parseRoleMap(ca.GetRoleMap())
return trace.Wrap(err)
}
func checkJWTKeys(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
// Check that some JWT keys have been set on the CA.
if len(ca.Spec.ActiveKeys.JWT) == 0 {
return trace.BadParameter("missing JWT CA")
}
var err error
var privateKey crypto.Signer
// Check that the JWT keys set are valid.
for _, pair := range ca.GetTrustedJWTKeyPairs() {
// TODO(nic): validate PKCS11 private keys
if len(pair.PrivateKey) > 0 && pair.PrivateKeyType == types.PrivateKeyType_RAW {
privateKey, err = keys.ParsePrivateKey(pair.PrivateKey)
if err != nil {
return trace.Wrap(err)
}
}
publicKey, err := keys.ParsePublicKey(pair.PublicKey)
if err != nil {
return trace.Wrap(err)
}
cfg := &jwt.Config{
ClusterName: ca.GetClusterName(),
PrivateKey: privateKey,
PublicKey: publicKey,
}
if _, err = jwt.New(cfg); err != nil {
return trace.Wrap(err)
}
}
return nil
}
// checkSAMLIDPCA checks if provided certificate authority contains a valid TLS key pair.
// This function is used to verify the SAML IDP CA.
func checkSAMLIDPCA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
return trace.Wrap(checkTLSKeys(ca))
}
func checkWindowsCA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
return trace.Wrap(checkTLSKeys(ca))
}
func checkAppClientCA(cai types.CertAuthority) error {
ca, ok := cai.(*types.CertAuthorityV2)
if !ok {
return trace.BadParameter("unknown CA type %T", cai)
}
return trace.Wrap(checkTLSKeys(ca))
}
func checkTLSKeys(ca *types.CertAuthorityV2) error {
if len(ca.Spec.ActiveKeys.TLS) == 0 {
return trace.BadParameter("%s certificate authority missing TLS key pairs", ca.GetType())
}
for _, pair := range ca.GetTrustedTLSKeyPairs() {
// Note: A non-empty pair.Cert is required by pair.CheckAndSetDefaults().
if len(pair.Key) > 0 && pair.KeyType == types.PrivateKeyType_RAW {
if _, err := tls.X509KeyPair(pair.Cert, pair.Key); err != nil {
return trace.Wrap(err, "private key and certificate")
}
continue
}
if _, err := tlsca.ParseCertificatePEM(pair.Cert); err != nil {
return trace.Wrap(err, "certificate")
}
}
return nil
}
// GetJWTSigner returns the active JWT key used to sign tokens.
func GetJWTSigner(signer crypto.Signer, clusterName string, clock clockwork.Clock) (*jwt.Key, error) {
key, err := jwt.New(&jwt.Config{
Clock: clock,
ClusterName: clusterName,
PrivateKey: signer,
})
return key, trace.Wrap(err)
}
// GetTLSCerts returns TLS certificates from CA
func GetTLSCerts(ca types.CertAuthority) [][]byte {
pairs := ca.GetTrustedTLSKeyPairs()
out := make([][]byte, len(pairs))
for i, pair := range pairs {
out[i] = slices.Clone(pair.Cert)
}
return out
}
// GetX509Certs returns parsed TLS certificates from CA as [x509.Certificate].
func GetX509Certs(ca types.CertAuthority) iter.Seq2[*x509.Certificate, error] {
pairs := ca.GetTrustedTLSKeyPairs()
return func(yield func(*x509.Certificate, error) bool) {
for _, pair := range pairs {
cert, err := tlsca.ParseCertificatePEM(pair.Cert)
if !yield(cert, err) {
return
}
}
}
}
// GetSSHCheckingKeys returns SSH public keys from CA
func GetSSHCheckingKeys(ca types.CertAuthority) [][]byte {
pairs := ca.GetTrustedSSHKeyPairs()
out := make([][]byte, 0, len(pairs))
for _, pair := range pairs {
out = append(out, slices.Clone(pair.PublicKey))
}
return out
}
// CertPoolFromCertAuthorities returns a certificate pool from the TLS certificates
// set up in the certificate authorities list, as well as the number of certificates
// that were added to the pool.
func CertPoolFromCertAuthorities(cas []types.CertAuthority) (*x509.CertPool, int, error) {
certPool := x509.NewCertPool()
count := 0
for _, ca := range cas {
for cert, err := range GetX509Certs(ca) {
if err != nil {
return nil, 0, trace.Wrap(err)
}
certPool.AddCert(cert)
count++
}
}
return certPool, count, nil
}
// CertPool returns certificate pools from TLS certificates
// set up in the certificate authority
func CertPool(ca types.CertAuthority) (*x509.CertPool, error) {
certPool, count, err := CertPoolFromCertAuthorities([]types.CertAuthority{ca})
if err != nil {
return nil, trace.Wrap(err)
}
if count == 0 {
return nil, trace.BadParameter("certificate authority has no TLS certificates")
}
return certPool, nil
}
// UnmarshalCertAuthority unmarshals the CertAuthority resource to JSON.
func UnmarshalCertAuthority(bytes []byte, opts ...MarshalOption) (types.CertAuthority, error) {
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
var h types.ResourceHeader
err = utils.FastUnmarshal(bytes, &h)
if err != nil {
return nil, trace.Wrap(err)
}
switch h.Version {
case types.V2:
var ca types.CertAuthorityV2
if err := utils.FastUnmarshal(bytes, &ca); err != nil {
return nil, trace.BadParameter("%s", err)
}
if err := ValidateCertAuthority(&ca); err != nil {
return nil, trace.Wrap(err)
}
if cfg.Revision != "" {
ca.SetRevision(cfg.Revision)
}
// Correct problems with existing CAs that contain non-UTC times, which
// causes panics when doing a gogoproto Clone; should only ever be
// possible with LastRotated, but we enforce it on all the times anyway.
// See https://github.com/gogo/protobuf/issues/519 .
if ca.Spec.Rotation != nil {
apiutils.UTC(&ca.Spec.Rotation.Started)
apiutils.UTC(&ca.Spec.Rotation.LastRotated)
apiutils.UTC(&ca.Spec.Rotation.Schedule.UpdateClients)
apiutils.UTC(&ca.Spec.Rotation.Schedule.UpdateServers)
apiutils.UTC(&ca.Spec.Rotation.Schedule.Standby)
}
return &ca, nil
}
return nil, trace.BadParameter("cert authority resource version %v is not supported", h.Version)
}
// MarshalCertAuthority marshals the CertAuthority resource to JSON.
func MarshalCertAuthority(certAuthority types.CertAuthority, opts ...MarshalOption) ([]byte, error) {
if err := ValidateCertAuthority(certAuthority); err != nil {
return nil, trace.Wrap(err)
}
cfg, err := CollectOptions(opts)
if err != nil {
return nil, trace.Wrap(err)
}
switch certAuthority := certAuthority.(type) {
case *types.CertAuthorityV2:
return utils.FastMarshal(maybeResetProtoRevision(cfg.PreserveRevision, certAuthority))
default:
return nil, trace.BadParameter("unrecognized certificate authority version %T", certAuthority)
}
}