1
16
17 package certificates
18
19 import (
20 "context"
21 "crypto/ecdsa"
22 "crypto/elliptic"
23 "crypto/rand"
24 "crypto/x509/pkix"
25 "encoding/pem"
26 "os"
27 "path"
28 "strings"
29 "testing"
30 "time"
31
32 "github.com/google/go-cmp/cmp"
33
34 certificatesv1 "k8s.io/api/certificates/v1"
35 v1 "k8s.io/api/core/v1"
36 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
38 "k8s.io/apimachinery/pkg/runtime/schema"
39 "k8s.io/apiserver/pkg/server/dynamiccertificates"
40 "k8s.io/client-go/informers"
41 clientset "k8s.io/client-go/kubernetes"
42 "k8s.io/client-go/rest"
43 certutil "k8s.io/client-go/util/cert"
44 "k8s.io/client-go/util/certificate/csr"
45 "k8s.io/client-go/util/keyutil"
46 "k8s.io/klog/v2/ktesting"
47 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
48 "k8s.io/kubernetes/pkg/controller/certificates/signer"
49 "k8s.io/kubernetes/test/integration/framework"
50 "k8s.io/utils/pointer"
51 )
52
53 func TestCSRDuration(t *testing.T) {
54 t.Parallel()
55
56 s := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
57 t.Cleanup(s.TearDownFn)
58
59 _, ctx := ktesting.NewTestContext(t)
60 ctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
61 t.Cleanup(cancel)
62
63
64
65 wantMetricStrings := []string{
66 `apiserver_certificates_registry_csr_honored_duration_total{signerName="kubernetes.io/kube-apiserver-client"} 6`,
67 `apiserver_certificates_registry_csr_requested_duration_total{signerName="kubernetes.io/kube-apiserver-client"} 7`,
68 }
69 t.Cleanup(func() {
70 copyConfig := rest.CopyConfig(s.ClientConfig)
71 copyConfig.GroupVersion = &schema.GroupVersion{}
72 copyConfig.NegotiatedSerializer = unstructuredscheme.NewUnstructuredNegotiatedSerializer()
73 rc, err := rest.RESTClientFor(copyConfig)
74 if err != nil {
75 t.Fatal(err)
76 }
77 body, err := rc.Get().AbsPath("/metrics").DoRaw(ctx)
78 if err != nil {
79 t.Fatal(err)
80 }
81 var gotMetricStrings []string
82 for _, line := range strings.Split(string(body), "\n") {
83 if strings.HasPrefix(line, "apiserver_certificates_registry_") {
84 gotMetricStrings = append(gotMetricStrings, line)
85 }
86 }
87 if diff := cmp.Diff(wantMetricStrings, gotMetricStrings); diff != "" {
88 t.Errorf("unexpected metrics diff (-want +got): %s", diff)
89 }
90 })
91
92 client := clientset.NewForConfigOrDie(s.ClientConfig)
93 informerFactory := informers.NewSharedInformerFactory(client, 0)
94
95 caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
96 if err != nil {
97 t.Fatal(err)
98 }
99 caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey)
100 if err != nil {
101 t.Fatal(err)
102 }
103 caPublicKeyFile := path.Join(s.TmpDir, "test-ca-public-key")
104 if err := os.WriteFile(caPublicKeyFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw}), os.FileMode(0600)); err != nil {
105 t.Fatal(err)
106 }
107 caPrivateKeyBytes, err := keyutil.MarshalPrivateKeyToPEM(caPrivateKey)
108 if err != nil {
109 t.Fatal(err)
110 }
111 caPrivateKeyFile := path.Join(s.TmpDir, "test-ca-private-key")
112 if err := os.WriteFile(caPrivateKeyFile, caPrivateKeyBytes, os.FileMode(0600)); err != nil {
113 t.Fatal(err)
114 }
115
116 c, err := signer.NewKubeAPIServerClientCSRSigningController(ctx, client, informerFactory.Certificates().V1().CertificateSigningRequests(), caPublicKeyFile, caPrivateKeyFile, 24*time.Hour)
117 if err != nil {
118 t.Fatal(err)
119 }
120
121 informerFactory.Start(ctx.Done())
122 go c.Run(ctx, 1)
123
124 tests := []struct {
125 name, csrName string
126 duration *time.Duration
127 wantDuration time.Duration
128 wantError string
129 }{
130 {
131 name: "no duration set",
132 duration: nil,
133 wantDuration: 24 * time.Hour,
134 wantError: "",
135 },
136 {
137 name: "same duration set as certTTL",
138 duration: pointer.Duration(24 * time.Hour),
139 wantDuration: 24 * time.Hour,
140 wantError: "",
141 },
142 {
143 name: "longer duration than certTTL",
144 duration: pointer.Duration(48 * time.Hour),
145 wantDuration: 24 * time.Hour,
146 wantError: "",
147 },
148 {
149 name: "slightly shorter duration set",
150 duration: pointer.Duration(20 * time.Hour),
151 wantDuration: 20 * time.Hour,
152 wantError: "",
153 },
154 {
155 name: "even shorter duration set",
156 duration: pointer.Duration(10 * time.Hour),
157 wantDuration: 10 * time.Hour,
158 wantError: "",
159 },
160 {
161 name: "short duration set",
162 duration: pointer.Duration(2 * time.Hour),
163 wantDuration: 2*time.Hour + 5*time.Minute,
164 wantError: "",
165 },
166 {
167 name: "very short duration set",
168 duration: pointer.Duration(30 * time.Minute),
169 wantDuration: 30*time.Minute + 5*time.Minute,
170 wantError: "",
171 },
172 {
173 name: "shortest duration set",
174 duration: pointer.Duration(10 * time.Minute),
175 wantDuration: 10*time.Minute + 5*time.Minute,
176 wantError: "",
177 },
178 {
179 name: "just too short duration set",
180 csrName: "invalid-csr-001",
181 duration: pointer.Duration(10*time.Minute - time.Second),
182 wantDuration: 0,
183 wantError: `cannot create certificate signing request: ` +
184 `CertificateSigningRequest.certificates.k8s.io "invalid-csr-001" is invalid: spec.expirationSeconds: Invalid value: 599: may not specify a duration less than 600 seconds (10 minutes)`,
185 },
186 {
187 name: "really too short duration set",
188 csrName: "invalid-csr-002",
189 duration: pointer.Duration(3 * time.Minute),
190 wantDuration: 0,
191 wantError: `cannot create certificate signing request: ` +
192 `CertificateSigningRequest.certificates.k8s.io "invalid-csr-002" is invalid: spec.expirationSeconds: Invalid value: 180: may not specify a duration less than 600 seconds (10 minutes)`,
193 },
194 {
195 name: "negative duration set",
196 csrName: "invalid-csr-003",
197 duration: pointer.Duration(-7 * time.Minute),
198 wantDuration: 0,
199 wantError: `cannot create certificate signing request: ` +
200 `CertificateSigningRequest.certificates.k8s.io "invalid-csr-003" is invalid: spec.expirationSeconds: Invalid value: -420: may not specify a duration less than 600 seconds (10 minutes)`,
201 },
202 }
203 for _, tt := range tests {
204 tt := tt
205 t.Run(tt.name, func(t *testing.T) {
206 t.Parallel()
207
208 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
209 if err != nil {
210 t.Fatal(err)
211 }
212 csrData, err := certutil.MakeCSR(privateKey, &pkix.Name{CommonName: "panda"}, nil, nil)
213 if err != nil {
214 t.Fatal(err)
215 }
216
217 csrName, csrUID, errReq := csr.RequestCertificate(client, csrData, tt.csrName, certificatesv1.KubeAPIServerClientSignerName,
218 tt.duration, []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth}, privateKey)
219
220 if diff := cmp.Diff(tt.wantError, errStr(errReq)); len(diff) > 0 {
221 t.Fatalf("CSR input duration %v err diff (-want, +got):\n%s", tt.duration, diff)
222 }
223
224 if len(tt.wantError) > 0 {
225 return
226 }
227
228 csrObj, err := client.CertificatesV1().CertificateSigningRequests().Get(ctx, csrName, metav1.GetOptions{})
229 if err != nil {
230 t.Fatal(err)
231 }
232 csrObj.Status.Conditions = []certificatesv1.CertificateSigningRequestCondition{
233 {
234 Type: certificatesv1.CertificateApproved,
235 Status: v1.ConditionTrue,
236 Reason: "TestCSRDuration",
237 Message: t.Name(),
238 },
239 }
240 _, err = client.CertificatesV1().CertificateSigningRequests().UpdateApproval(ctx, csrName, csrObj, metav1.UpdateOptions{})
241 if err != nil {
242 t.Fatal(err)
243 }
244
245 certData, err := csr.WaitForCertificate(ctx, client, csrName, csrUID)
246 if err != nil {
247 t.Fatal(err)
248 }
249
250 certs, err := certutil.ParseCertsPEM(certData)
251 if err != nil {
252 t.Fatal(err)
253 }
254
255 switch l := len(certs); l {
256 case 1:
257
258 default:
259 t.Errorf("expected 1 cert, got %d", l)
260 for i, certificate := range certs {
261 t.Log(i, dynamiccertificates.GetHumanCertDetail(certificate))
262 }
263 t.FailNow()
264 }
265
266 cert := certs[0]
267
268 if got := cert.NotAfter.Sub(cert.NotBefore); got != tt.wantDuration {
269 t.Errorf("CSR input duration %v got duration = %v, want %v\n%s", tt.duration, got, tt.wantDuration, dynamiccertificates.GetHumanCertDetail(cert))
270 }
271 })
272 }
273 }
274
275 func errStr(err error) string {
276 if err == nil {
277 return ""
278 }
279 es := err.Error()
280 if len(es) == 0 {
281 panic("invalid empty error")
282 }
283 return es
284 }
285
View as plain text