CVE-2022-36103 (published 2022-09-13T17:15Z)

Talos Linux is a Linux distribution built for Kubernetes deployments. Talos worker nodes use a join token to get accepted into the Talos cluster. Due to improper validation of the request while signing a worker node CSR (certificate signing request) Talos control plane node might issue Talos API certificate which allows full access to Talos API on a control plane node. Accessing Talos API with full level access on a control plane node might reveal sensitive information which allows full level access to the cluster (Kubernetes and Talos PKI, etc.). Talos API join token is stored in the machine configuration on the worker node. When configured correctly, Kubernetes workloads don't have access to the machine configuration, but due to a misconfiguration workload might access the machine configuration and reveal the join token. This problem has been fixed in Talos 1.2.2. Enabling the Pod Security Standards mitigates the vulnerability by denying hostPath mounts and host networking by default in the baseline policy. Clusters that don't run untrusted workloads are not affected. Clusters with correct Pod Security configurations which don't allow hostPath mounts, and secure access to cloud metadata server (or machine configuration is not supplied via cloud metadata server) are not affected.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-863
cwe_name: Incorrect Authorization
description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are incorrectly applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/siderolabs/talos
commit_details: repo_url: https://github.com/siderolabs/talos
author: Andrey Smirnov
author_date: 2022-09-08 15:58:10+04:00
msg: fix: never sign client certificate requests in trustd

Talos worker nodes use `trustd` API on control plane nodes to issue
certificates for `apid` service. Access to the API is protected with the
Talos join token specified in the machine configuration.

There was no validation on what kind of request is requested, so
`trustd` could issue a certificate which is valid for client
authentication with any set of Talos API RBAC roles, including
`os:admin` role allowing full access to the Talos API on control plane
nodes.

See: GHSA-7hgc-php5-77qq
CVE: CVE-2022-36103

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
repository: repo_url: https://github.com/siderolabs/talos
repo_name: siderolabs/talos
description: Talos Linux is a modern Linux distribution built for Kubernetes.
repo_language: Go
owner: siderolabs
file_changes: filename: reg.go
change_type: ModificationType.MODIFY
diff: @@ -6,12 +6,19 @@ package reg
 
 import (
 	"context"
+	stdx509 "crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"log"
 
 	"github.com/cosi-project/runtime/pkg/resource"
 	"github.com/cosi-project/runtime/pkg/safe"
 	"github.com/cosi-project/runtime/pkg/state"
 	"github.com/siderolabs/crypto/x509"
 	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/peer"
+	"google.golang.org/grpc/status"
 
 	securityapi "github.com/talos-systems/talos/pkg/machinery/api/security"
 	"github.com/talos-systems/talos/pkg/machinery/resources/secrets"
@@ -33,17 +40,63 @@ func (r *Registrator) Register(s *grpc.Server) {
 }
 
 // Certificate implements the securityapi.SecurityServer interface.
+//
+// This API is called by Talos worker nodes to request a server certificate for apid running on the node.
+// Control plane nodes generate certificates (client and server) directly from machine config PKI.
 func (r *Registrator) Certificate(ctx context.Context, in *securityapi.CertificateRequest) (resp *securityapi.CertificateResponse, err error) {
+	remotePeer, ok := peer.FromContext(ctx)
+	if !ok {
+		return nil, status.Error(codes.PermissionDenied, "peer not found")
+	}
+
 	osRoot, err := safe.StateGet[*secrets.OSRoot](ctx, r.Resources, resource.NewMetadata(secrets.NamespaceName, secrets.OSRootType, secrets.OSRootID, resource.VersionUndefined))
 	if err != nil {
 		return nil, err
 	}
 
-	// TODO: Verify that the request is coming from the IP addresss declared in
+	// decode and validate CSR
+	csrPemBlock, _ := pem.Decode(in.Csr)
+	if csrPemBlock == nil {
+		return nil, status.Errorf(codes.InvalidArgument, "failed to decode CSR")
+	}
+
+	request, err := stdx509.ParseCertificateRequest(csrPemBlock.Bytes)
+	if err != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "failed to parse CSR: %s", err)
+	}
+
+	log.Printf("received CSR signing request from %s: subject %s dns names %s addresses %s", remotePeer.Addr, request.Subject, request.DNSNames, request.IPAddresses)
+
+	// allow only server auth certificates
+	x509Opts := []x509.Option{
+		x509.KeyUsage(stdx509.KeyUsageDigitalSignature),
+		x509.ExtKeyUsage([]stdx509.ExtKeyUsage{stdx509.ExtKeyUsageServerAuth}),
+	}
+
+	// don't allow any certificates which can be used for client authentication
+	//
+	// we don't return an error here, as otherwise workers running old versions of Talos
+	// will fail to provision client certificate and will never launch apid
+	//
+	// instead, the returned certificate will be rejected when being used
+	if len(request.Subject.Organization) > 0 {
+		log.Printf("removing client auth organization from CSR: %s", request.Subject.Organization)
+
+		x509Opts = append(x509Opts, x509.OverrideSubject(func(subject *pkix.Name) {
+			subject.Organization = nil
+		}))
+	}
+
+	// TODO: Verify that the request is coming from the IP address declared in
 	// the CSR.
-	signed, err := x509.NewCertificateFromCSRBytes(osRoot.TypedSpec().CA.Crt, osRoot.TypedSpec().CA.Key, in.Csr)
+	signed, err := x509.NewCertificateFromCSRBytes(
+		osRoot.TypedSpec().CA.Crt,
+		osRoot.TypedSpec().CA.Key,
+		in.Csr,
+		x509Opts...,
+	)
 	if err != nil {
-		return
+		return nil, status.Errorf(codes.Internal, "failed to sign CSR: %s", err)
 	}
 
 	resp = &securityapi.CertificateResponse{

programming_language: Go
filename: reg_test.go
change_type: ModificationType.MODIFY
diff: @@ -4,11 +4,98 @@
 
 package reg_test
 
-import "testing"
+import (
+	"context"
+	stdx509 "crypto/x509"
+	"net"
+	"net/netip"
+	"testing"
+	"time"
 
-func TestEmpty(t *testing.T) {
-	// added for accurate coverage estimation
-	//
-	// please remove it once any unit-test is added
-	// for this package
+	"github.com/cosi-project/runtime/pkg/state"
+	"github.com/cosi-project/runtime/pkg/state/impl/inmem"
+	"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
+	"github.com/siderolabs/crypto/x509"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"google.golang.org/grpc/peer"
+
+	"github.com/talos-systems/talos/internal/app/trustd/internal/reg"
+	"github.com/talos-systems/talos/pkg/machinery/api/security"
+	"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate"
+	"github.com/talos-systems/talos/pkg/machinery/resources/secrets"
+	"github.com/talos-systems/talos/pkg/machinery/role"
+)
+
+func TestCertificate(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	resources := state.WrapCore(namespaced.NewState(inmem.Build))
+
+	ca, err := generate.NewTalosCA(time.Now())
+	require.NoError(t, err)
+
+	osRoot := secrets.NewOSRoot(secrets.OSRootID)
+	osRoot.TypedSpec().CA = &x509.PEMEncodedCertificateAndKey{
+		Crt: ca.CrtPEM,
+		Key: ca.KeyPEM,
+	}
+	require.NoError(t, resources.Create(ctx, osRoot))
+
+	ctx = peer.NewContext(ctx, &peer.Peer{
+		Addr: &net.TCPAddr{
+			IP:   netip.MustParseAddr("127.0.0.1").AsSlice(),
+			Port: 30000,
+		},
+	})
+
+	r := &reg.Registrator{
+		Resources: resources,
+	}
+
+	for _, tt := range []struct {
+		name       string
+		csrSetters []x509.Option
+	}{
+		{
+			name: "server certificate",
+			csrSetters: []x509.Option{
+				x509.IPAddresses([]net.IP{netip.MustParseAddr("10.5.0.4").AsSlice()}),
+				x509.DNSNames([]string{"talos-default-worker-1"}),
+				x509.CommonName("talos-default-worker-1"),
+			},
+		},
+		{
+			name: "attempt at client certificate",
+			csrSetters: []x509.Option{
+				x509.CommonName("talos-default-worker-1"),
+				x509.Organization(string(role.Impersonator)),
+			},
+		},
+	} {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			serverCSR, serverCert, err := x509.NewEd25519CSRAndIdentity(tt.csrSetters...)
+			require.NoError(t, err)
+
+			resp, err := r.Certificate(ctx, &security.CertificateRequest{
+				Csr: serverCSR.X509CertificateRequestPEM,
+			})
+			require.NoError(t, err)
+
+			assert.Equal(t, resp.Ca, ca.CrtPEM)
+
+			serverCert.Crt = resp.Crt
+
+			cert, err := serverCert.GetCert()
+			require.NoError(t, err)
+
+			assert.Equal(t, stdx509.KeyUsageDigitalSignature, cert.KeyUsage)
+			assert.Equal(t, []stdx509.ExtKeyUsage{stdx509.ExtKeyUsageServerAuth}, cert.ExtKeyUsage)
+			assert.Equal(t, "talos-default-worker-1", cert.Subject.CommonName)
+			assert.Equal(t, []string(nil), cert.Subject.Organization)
+		})
+	}
 }

programming_language: Go