Skip to content

Commit 97702bb

Browse files
authored
Merge pull request kubernetes#134778 from yt2985/podcertificates-agnhost-2
Add mtlsclient and mtlsserver for the mtls validations
2 parents 3c4a718 + 9e5b6ad commit 97702bb

File tree

5 files changed

+360
-2
lines changed

5 files changed

+360
-2
lines changed

test/images/agnhost/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,33 @@ Usage:
367367
[--retry_time <seconds>] [--break_on_expected_content <true_or_false>]
368368
```
369369

370+
### mtlsclient
371+
372+
```console
373+
kubectl run test-agnhost \
374+
--generator=run-pod/v1 \
375+
--image=registry.k8s.io/e2e-test-images/agnhost:2.58 \
376+
--restart=Always \
377+
-- \
378+
mtlsclient \
379+
--fetch-url=<server-address> \
380+
--server-trust-bundle=<server-trust-bundle.pem> \
381+
--client-cred-bundle=<client-cred-bundle.pem>
382+
```
383+
384+
### mtlsserver
385+
386+
```console
387+
kubectl run test-agnhost \
388+
--generator=run-pod/v1 \
389+
--image=registry.k8s.io/e2e-test-images/agnhost:2.58 \
390+
--restart=Always \
391+
-- \
392+
mtlsserver \
393+
--listen=<0.0.0.0:443> \
394+
--server-creds=<server-cred-bundle.pem> \
395+
--spiffe-trust-bundle=<spiffe-trust-bundle.pem>
396+
```
370397

371398
### net
372399

test/images/agnhost/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.57
1+
2.58

test/images/agnhost/agnhost.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import (
3535
"k8s.io/kubernetes/test/images/agnhost/liveness"
3636
logsgen "k8s.io/kubernetes/test/images/agnhost/logs-generator"
3737
"k8s.io/kubernetes/test/images/agnhost/mounttest"
38+
"k8s.io/kubernetes/test/images/agnhost/mtlsclient"
39+
"k8s.io/kubernetes/test/images/agnhost/mtlsserver"
3840
"k8s.io/kubernetes/test/images/agnhost/net"
3941
"k8s.io/kubernetes/test/images/agnhost/netexec"
4042
"k8s.io/kubernetes/test/images/agnhost/nettest"
@@ -92,7 +94,8 @@ func main() {
9294
rootCmd.AddCommand(grpchealthchecking.CmdGrpcHealthChecking)
9395
rootCmd.AddCommand(vishhstress.CmdStress)
9496
rootCmd.AddCommand(podcertificatesigner.CmdPodCertificateSigner)
95-
97+
rootCmd.AddCommand(mtlsclient.CmdMtlsClient)
98+
rootCmd.AddCommand(mtlsserver.CmdMtlsServer)
9699
// NOTE(claudiub): Some tests are passing logging related flags, so we need to be able to
97100
// accept them. This will also include them in the printed help.
98101
code := cli.Run(rootCmd)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package mtlsclient is an agnhost subcommand implementing a client to build
18+
// the mTLS with the server.
19+
package mtlsclient
20+
21+
import (
22+
"crypto/tls"
23+
"crypto/x509"
24+
"encoding/pem"
25+
"flag"
26+
"fmt"
27+
"io"
28+
"log"
29+
"net/http"
30+
"net/url"
31+
"os"
32+
"time"
33+
34+
"github.com/spf13/cobra"
35+
"k8s.io/component-base/logs"
36+
"k8s.io/klog/v2"
37+
)
38+
39+
var CmdMtlsClient = &cobra.Command{
40+
Use: "mtlsclient",
41+
Short: "Client is configured to build mTLS with server",
42+
Args: cobra.MaximumNArgs(0),
43+
RunE: main,
44+
}
45+
46+
var (
47+
fetchURL string
48+
serverTrustBundleFile string
49+
clientCredBundleFile string
50+
)
51+
52+
func init() {
53+
CmdMtlsClient.Flags().StringVar(&fetchURL, "fetch-url", "", "server URL to poll")
54+
CmdMtlsClient.Flags().StringVar(&serverTrustBundleFile, "server-trust-bundle", "", "File with trust anchors to verify the server certificate")
55+
CmdMtlsClient.Flags().StringVar(&clientCredBundleFile, "client-cred-bundle", "", "File with client key and certificate chain")
56+
}
57+
58+
func main(cmd *cobra.Command, args []string) error {
59+
logs.InitLogs()
60+
defer logs.FlushLogs()
61+
if err := flag.Set("logtostderr", "true"); err != nil {
62+
return fmt.Errorf("failed to set flags: %w", err)
63+
}
64+
65+
if fetchURL == "" || serverTrustBundleFile == "" || clientCredBundleFile == "" {
66+
return fmt.Errorf("missing required flags")
67+
}
68+
69+
if _, err := url.ParseRequestURI(fetchURL); err != nil {
70+
return fmt.Errorf("invalid --fetch-url: %w", err)
71+
}
72+
73+
for range time.Tick(10 * time.Second) {
74+
if err := pollOnce(); err != nil {
75+
klog.Errorf("while sending requests to the server: %v", err)
76+
}
77+
}
78+
return nil
79+
}
80+
81+
func pollOnce() error {
82+
trustBundlePEM, err := os.ReadFile(serverTrustBundleFile)
83+
if err != nil {
84+
return fmt.Errorf("while reading service trust bundle: %w", err)
85+
}
86+
87+
serverTrustAnchors := x509.NewCertPool()
88+
serverTrustAnchors.AppendCertsFromPEM(trustBundlePEM)
89+
90+
tlsConfig := &tls.Config{
91+
RootCAs: serverTrustAnchors,
92+
}
93+
94+
// Load and send client certificates if a bundle file was specified.
95+
if clientCredBundleFile != "" {
96+
bundlePEM, err := os.ReadFile(clientCredBundleFile)
97+
if err != nil {
98+
return fmt.Errorf("while reading client credential bundle: %w", err)
99+
}
100+
101+
cert := tls.Certificate{}
102+
103+
var block *pem.Block
104+
rest := bundlePEM
105+
for {
106+
block, rest = pem.Decode(rest)
107+
if block == nil {
108+
break
109+
}
110+
111+
switch block.Type {
112+
case "PRIVATE KEY":
113+
cert.PrivateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
114+
if err != nil {
115+
return fmt.Errorf("while parsing private key from credential bundle: %w", err)
116+
}
117+
case "CERTIFICATE":
118+
cert.Certificate = append(cert.Certificate, block.Bytes)
119+
}
120+
}
121+
122+
if cert.PrivateKey == nil {
123+
return fmt.Errorf("client credential bundle had no private key")
124+
}
125+
126+
if len(cert.Certificate) == 0 {
127+
return fmt.Errorf("client credential bundle had no certificates")
128+
}
129+
130+
tlsConfig.Certificates = []tls.Certificate{cert}
131+
}
132+
133+
client := &http.Client{
134+
Transport: &http.Transport{
135+
TLSClientConfig: tlsConfig,
136+
},
137+
}
138+
139+
resp, err := client.Get(fetchURL)
140+
if err != nil {
141+
return fmt.Errorf("while getting URL: %w", err)
142+
143+
}
144+
145+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
146+
return fmt.Errorf("non-2xx status %d %q", resp.StatusCode, resp.Status)
147+
}
148+
149+
body, err := io.ReadAll(resp.Body)
150+
if err != nil {
151+
return fmt.Errorf("while reading body: %w", err)
152+
}
153+
defer func() {
154+
if err := resp.Body.Close(); err != nil {
155+
log.Printf("while closing response body: %v", err)
156+
}
157+
}()
158+
159+
log.Printf("Got response body: %s", string(body))
160+
return nil
161+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package mtlsserver is an agnhost subcommand implementing a server to build
18+
// the mTLS with the client and echo the client identity.
19+
package mtlsserver
20+
21+
import (
22+
"crypto/tls"
23+
"crypto/x509"
24+
"flag"
25+
"fmt"
26+
"log"
27+
"net/http"
28+
"os"
29+
"time"
30+
31+
"github.com/spf13/cobra"
32+
"k8s.io/component-base/logs"
33+
)
34+
35+
var CmdMtlsServer = &cobra.Command{
36+
Use: "mtlsserver",
37+
Short: "Server is configured to build mTLS with client and echo client's spiffe identity",
38+
Args: cobra.MaximumNArgs(0),
39+
RunE: main,
40+
}
41+
42+
var (
43+
listen string
44+
serverCredsFile string
45+
spiffeTrustBundleFile string
46+
)
47+
48+
func init() {
49+
CmdMtlsServer.Flags().StringVar(&listen, "listen", "", "<address>:<port> to listen on")
50+
CmdMtlsServer.Flags().StringVar(&serverCredsFile, "server-creds", "", "Credential bundle with the server key and certificate chain")
51+
CmdMtlsServer.Flags().StringVar(&spiffeTrustBundleFile, "spiffe-trust-bundle", "", "Trust bundle for verifying client certificates")
52+
}
53+
54+
func main(cmd *cobra.Command, args []string) error {
55+
logs.InitLogs()
56+
defer logs.FlushLogs()
57+
if err := flag.Set("logtostderr", "true"); err != nil {
58+
return fmt.Errorf("failed to set flags: %w", err)
59+
}
60+
61+
if listen == "" || serverCredsFile == "" || spiffeTrustBundleFile == "" {
62+
return fmt.Errorf("missing required flags")
63+
}
64+
65+
if err := serve(); err != nil {
66+
return fmt.Errorf("error while serving: %w", err)
67+
}
68+
return nil
69+
}
70+
71+
func serve() error {
72+
serveMux := http.NewServeMux()
73+
serveMux.HandleFunc("GET /spiffe-echo", handleGetSPIFFEEcho)
74+
75+
clientTrustBundlePEM, err := os.ReadFile(spiffeTrustBundleFile)
76+
if err != nil {
77+
return fmt.Errorf("while reading SPIFFE trust anchors: %w", err)
78+
}
79+
80+
rootPool := x509.NewCertPool()
81+
if ok := rootPool.AppendCertsFromPEM(clientTrustBundlePEM); !ok {
82+
return fmt.Errorf("failed to append client certs from PEM")
83+
}
84+
85+
// Pre-load server cert to check early if it's readable
86+
_, err = tls.LoadX509KeyPair(serverCredsFile, serverCredsFile)
87+
if err != nil {
88+
return fmt.Errorf("while loading server key pair: %w", err)
89+
}
90+
91+
server := &http.Server{
92+
Addr: listen,
93+
94+
Handler: serveMux,
95+
96+
TLSConfig: &tls.Config{
97+
// Tell the client to send client certs if they have any. Don't
98+
// pass in roots to verify the client certificate, since we expect
99+
// multiple types signed by different CAs. Instead, each endpoint
100+
// will do the appropriate client certificate verification.
101+
ClientAuth: tls.RequestClientCert,
102+
},
103+
104+
ReadTimeout: 30 * time.Second,
105+
WriteTimeout: 30 * time.Second,
106+
MaxHeaderBytes: 1 << 20,
107+
}
108+
109+
// TODO: Auto-reload the server creds
110+
if err := server.ListenAndServeTLS(serverCredsFile, serverCredsFile); err != nil {
111+
return fmt.Errorf("while listening: %w", err)
112+
}
113+
114+
return nil
115+
}
116+
117+
func handleGetSPIFFEEcho(w http.ResponseWriter, r *http.Request) {
118+
if len(r.TLS.PeerCertificates) == 0 {
119+
http.Error(w, "SPIFFE client certificate authentication required", http.StatusUnauthorized)
120+
return
121+
}
122+
123+
leafCert := r.TLS.PeerCertificates[0]
124+
125+
// TODO: Use the trust domain from the leaf certificate to select which
126+
// trust bundle to use, to permit federated use cases.
127+
128+
intermediatePool := x509.NewCertPool()
129+
if len(r.TLS.PeerCertificates) > 1 {
130+
for _, intermediate := range r.TLS.PeerCertificates[1:] {
131+
intermediatePool.AddCert(intermediate)
132+
}
133+
}
134+
rootTrustBundlePEM, err := os.ReadFile(spiffeTrustBundleFile)
135+
if err != nil {
136+
log.Printf("Error while reading SPIFFE trust anchors: %v", err)
137+
http.Error(w, "Internal Error", http.StatusInternalServerError)
138+
return
139+
}
140+
rootPool := x509.NewCertPool()
141+
rootPool.AppendCertsFromPEM(rootTrustBundlePEM)
142+
143+
chains, err := leafCert.Verify(x509.VerifyOptions{
144+
Intermediates: intermediatePool,
145+
Roots: rootPool,
146+
})
147+
if err != nil {
148+
log.Printf("Error while verifying SPIFFE client certificate: %v", err)
149+
http.Error(w, "SPIFFE client certificate authentication required", http.StatusUnauthorized)
150+
return
151+
}
152+
if len(chains) == 0 {
153+
log.Print("Client certificate did not chain to any roots")
154+
http.Error(w, "SPIFFE client certificate authentication required", http.StatusUnauthorized)
155+
return
156+
}
157+
158+
if len(leafCert.URIs) != 1 {
159+
log.Printf("SPIFFE client certificate did not have 1 URI SAN, count: %d", len(leafCert.URIs))
160+
http.Error(w, "Malformed SPIFFE certificate", http.StatusUnauthorized)
161+
return
162+
}
163+
164+
if _, err := w.Write([]byte("Client Identity: " + leafCert.URIs[0].String())); err != nil {
165+
log.Printf("Error while writing response: %v", err)
166+
}
167+
}

0 commit comments

Comments
 (0)