forked from kubernetes/kops
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Perform challenge callbacks into a node
In order to verify that the caller is running on the specified node, we source the expected IP address from the cloud, and require that the node set up a simple challenge/response server to answer requests. Because the challenge server runs on a port outside of the nodePort range, this also makes it harder for pods to impersonate their host nodes - though we do combine this with TPM and similar functionality where it is available.
- Loading branch information
Showing
23 changed files
with
653 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* | ||
Copyright 2023 The Kubernetes Authors. | ||
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 bootstrap | ||
|
||
import ( | ||
cryptorand "crypto/rand" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"crypto/x509/pkix" | ||
"fmt" | ||
"math/big" | ||
"time" | ||
|
||
"k8s.io/klog/v2" | ||
"k8s.io/kops/pkg/pki" | ||
) | ||
|
||
func randomBytes(length int) []byte { | ||
b := make([]byte, length) | ||
if _, err := cryptorand.Read(b); err != nil { | ||
klog.Fatalf("failed to read from crypto/rand: %v", err) | ||
} | ||
return b | ||
} | ||
|
||
func challengeKopsControllerSubject(clusterName string) pkix.Name { | ||
// Note: keep in sync with subjectsMatch if you add (additional) fields here | ||
return pkix.Name{ | ||
CommonName: "kops-controller." + clusterName, | ||
} | ||
} | ||
|
||
func subjectsMatch(l, r pkix.Name) bool { | ||
// We need to check all the fields in challengeKopsControllerSubject | ||
return l.CommonName == r.CommonName | ||
} | ||
|
||
func challengeServerHostName(clusterName string) string { | ||
return "challenge-server." + clusterName | ||
} | ||
|
||
func BuildChallengeServerCertificate(clusterName string) (*tls.Certificate, error) { | ||
serverName := challengeServerHostName(clusterName) | ||
|
||
privateKey, err := pki.GeneratePrivateKey() | ||
if err != nil { | ||
return nil, fmt.Errorf("generating ecdsa key: %w", err) | ||
} | ||
|
||
keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | ||
|
||
now := time.Now() | ||
notBefore := now.Add(-15 * time.Minute) | ||
notAfter := notBefore.Add(time.Hour) | ||
|
||
template := x509.Certificate{ | ||
SerialNumber: big.NewInt(1), | ||
Subject: pkix.Name{ | ||
CommonName: serverName, | ||
}, | ||
NotBefore: notBefore, | ||
NotAfter: notAfter, | ||
|
||
KeyUsage: keyUsage, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||
BasicConstraintsValid: true, | ||
} | ||
|
||
template.DNSNames = append(template.DNSNames, serverName) | ||
|
||
der, err := x509.CreateCertificate(cryptorand.Reader, &template, &template, privateKey.Key.Public(), privateKey.Key) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create certificate: %w", err) | ||
} | ||
|
||
parsed, err := x509.ParseCertificate(der) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse certificate: %w", err) | ||
} | ||
tlsCertificate := &tls.Certificate{ | ||
PrivateKey: privateKey.Key, | ||
Certificate: [][]byte{parsed.Raw}, | ||
Leaf: parsed, | ||
} | ||
|
||
return tlsCertificate, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/* | ||
Copyright 2023 The Kubernetes Authors. | ||
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 bootstrap | ||
|
||
import ( | ||
"context" | ||
"crypto/subtle" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"fmt" | ||
"time" | ||
|
||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/credentials" | ||
"k8s.io/kops/pkg/apis/nodeup" | ||
"k8s.io/kops/pkg/pki" | ||
pb "k8s.io/kops/proto/kops/bootstrap/v1" | ||
"k8s.io/kops/upup/pkg/fi" | ||
) | ||
|
||
type ChallengeClient struct { | ||
keystore pki.Keystore | ||
} | ||
|
||
func NewChallengeClient(keystore pki.Keystore) (*ChallengeClient, error) { | ||
return &ChallengeClient{ | ||
keystore: keystore, | ||
}, nil | ||
} | ||
|
||
func (c *ChallengeClient) getClientCertificate(ctx context.Context, clusterName string) (*tls.Certificate, error) { | ||
subject := challengeKopsControllerSubject(clusterName) | ||
|
||
certificate, privateKey, _, err := pki.IssueCert(ctx, &pki.IssueCertRequest{ | ||
Validity: 1 * time.Hour, | ||
Signer: fi.CertificateIDCA, | ||
Type: "client", | ||
Subject: subject, | ||
}, c.keystore) | ||
if err != nil { | ||
return nil, fmt.Errorf("error creating certificate: %w", err) | ||
} | ||
|
||
// TODO: Caching and rotation | ||
clientCertificate := &tls.Certificate{ | ||
PrivateKey: privateKey.Key, | ||
Certificate: [][]byte{certificate.Certificate.Raw}, | ||
Leaf: certificate.Certificate, | ||
} | ||
return clientCertificate, nil | ||
} | ||
|
||
func (c *ChallengeClient) DoCallbackChallenge(ctx context.Context, clusterName string, targetEndpoint string, bootstrapRequest *nodeup.BootstrapRequest) error { | ||
challenge := bootstrapRequest.Challenge | ||
|
||
if challenge == nil { | ||
return fmt.Errorf("challenge not set") | ||
} | ||
if challenge.ChallengeID == "" { | ||
return fmt.Errorf("challenge.id not set") | ||
} | ||
if len(challenge.ChallengeSecret) == 0 { | ||
return fmt.Errorf("challenge.secret not set") | ||
} | ||
if challenge.Endpoint == "" { | ||
return fmt.Errorf("challenge.endpoint not set") | ||
} | ||
if len(challenge.ServerCA) == 0 { | ||
return fmt.Errorf("challenge.ca not set") | ||
} | ||
|
||
clientCertificate, err := c.getClientCertificate(ctx, clusterName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
serverCAs := x509.NewCertPool() | ||
if !serverCAs.AppendCertsFromPEM(challenge.ServerCA) { | ||
return fmt.Errorf("error loading certificate pool") | ||
} | ||
|
||
serverName := challengeServerHostName(clusterName) | ||
tlsConfig := &tls.Config{ | ||
RootCAs: serverCAs, | ||
Certificates: []tls.Certificate{*clientCertificate}, | ||
ServerName: serverName, | ||
} | ||
|
||
kospControllerNonce := randomBytes(16) | ||
req := &pb.ChallengeRequest{ | ||
ChallengeId: challenge.ChallengeID, | ||
ChallengeRandom: kospControllerNonce, | ||
} | ||
|
||
expectedChallengeResponse := buildChallengeResponse(challenge.ChallengeSecret, kospControllerNonce) | ||
|
||
var opts []grpc.DialOption | ||
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) | ||
conn, err := grpc.DialContext(ctx, targetEndpoint, opts...) | ||
if err != nil { | ||
return fmt.Errorf("error dialing target %q: %w", targetEndpoint, err) | ||
} | ||
client := pb.NewCallbackServiceClient(conn) | ||
|
||
response, err := client.Challenge(ctx, req) | ||
if err != nil { | ||
return fmt.Errorf("error from callback challenge: %w", err) | ||
} | ||
|
||
if subtle.ConstantTimeCompare(response.GetChallengeResponse(), expectedChallengeResponse) != 1 { | ||
return fmt.Errorf("callback challenge returned wrong result") | ||
} | ||
return nil | ||
} |
Oops, something went wrong.