Skip to content

Commit 81a61ae

Browse files
authored
Merge pull request kubernetes#77863 from fabriziopandini/certs-expiration
Kubeadm: Add check certificate expiration command
2 parents c854f72 + e4d87b0 commit 81a61ae

File tree

10 files changed

+258
-21
lines changed

10 files changed

+258
-21
lines changed

cmd/kubeadm/app/cmd/alpha/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ go_library(
3131
"//cmd/kubeadm/app/util/config:go_default_library",
3232
"//cmd/kubeadm/app/util/kubeconfig:go_default_library",
3333
"//pkg/util/normalizer:go_default_library",
34+
"//staging/src/k8s.io/apimachinery/pkg/util/duration:go_default_library",
3435
"//staging/src/k8s.io/apimachinery/pkg/util/version:go_default_library",
3536
"//vendor/github.com/pkg/errors:go_default_library",
3637
"//vendor/github.com/spf13/cobra:go_default_library",

cmd/kubeadm/app/cmd/alpha/alpha.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func NewCmdAlpha(in io.Reader, out io.Writer) *cobra.Command {
3030
Short: "Kubeadm experimental sub-commands",
3131
}
3232

33-
cmd.AddCommand(newCmdCertsUtility())
33+
cmd.AddCommand(newCmdCertsUtility(out))
3434
cmd.AddCommand(newCmdKubeletUtility())
3535
cmd.AddCommand(newCmdKubeConfigUtility(out))
3636
cmd.AddCommand(NewCmdSelfhosting(in))

cmd/kubeadm/app/cmd/alpha/certs.go

+79-4
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ package alpha
1818

1919
import (
2020
"fmt"
21+
"io"
22+
"text/tabwriter"
2123

2224
"github.com/pkg/errors"
2325
"github.com/spf13/cobra"
2426

27+
"k8s.io/apimachinery/pkg/util/duration"
2528
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
2629
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
2730
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
@@ -54,17 +57,22 @@ var (
5457
Renew all known certificates necessary to run the control plane. Renewals are run unconditionally, regardless
5558
of expiration date. Renewals can also be run individually for more control.
5659
`)
60+
61+
expirationLongDesc = normalizer.LongDesc(`
62+
Checks expiration for the certificates in the local PKI managed by kubeadm.
63+
`)
5764
)
5865

5966
// newCmdCertsUtility returns main command for certs phase
60-
func newCmdCertsUtility() *cobra.Command {
67+
func newCmdCertsUtility(out io.Writer) *cobra.Command {
6168
cmd := &cobra.Command{
6269
Use: "certs",
6370
Aliases: []string{"certificates"},
6471
Short: "Commands related to handling kubernetes certificates",
6572
}
6673

6774
cmd.AddCommand(newCmdCertsRenewal())
75+
cmd.AddCommand(newCmdCertsExpiration(out, kubeadmconstants.KubernetesDir))
6876
return cmd
6977
}
7078

@@ -118,7 +126,7 @@ func getRenewSubCommands(kdir string) []*cobra.Command {
118126
Short: fmt.Sprintf("Renew the %s", handler.LongName),
119127
Long: fmt.Sprintf(genericCertRenewLongDesc, handler.LongName),
120128
}
121-
addFlags(cmd, flags)
129+
addRenewFlags(cmd, flags)
122130
// get the implementation of renewing this certificate
123131
renewalFunc := func(handler *renewal.CertificateRenewHandler) func() {
124132
return func() { renewCert(flags, kdir, handler) }
@@ -140,13 +148,13 @@ func getRenewSubCommands(kdir string) []*cobra.Command {
140148
}
141149
},
142150
}
143-
addFlags(allCmd, flags)
151+
addRenewFlags(allCmd, flags)
144152

145153
cmdList = append(cmdList, allCmd)
146154
return cmdList
147155
}
148156

149-
func addFlags(cmd *cobra.Command, flags *renewFlags) {
157+
func addRenewFlags(cmd *cobra.Command, flags *renewFlags) {
150158
options.AddConfigFlag(cmd.Flags(), &flags.cfgPath)
151159
options.AddCertificateDirFlag(cmd.Flags(), &flags.cfg.CertificatesDir)
152160
options.AddKubeConfigFlag(cmd.Flags(), &flags.kubeconfigPath)
@@ -197,3 +205,70 @@ func renewCert(flags *renewFlags, kdir string, handler *renewal.CertificateRenew
197205
}
198206
fmt.Printf("%s renewed\n", handler.LongName)
199207
}
208+
209+
// newCmdCertsExpiration creates a new `cert check-expiration` command.
210+
func newCmdCertsExpiration(out io.Writer, kdir string) *cobra.Command {
211+
flags := &expirationFlags{
212+
cfg: kubeadmapiv1beta2.InitConfiguration{
213+
ClusterConfiguration: kubeadmapiv1beta2.ClusterConfiguration{
214+
// Setting kubernetes version to a default value in order to allow a not necessary internet lookup
215+
KubernetesVersion: constants.CurrentKubernetesVersion.String(),
216+
},
217+
},
218+
}
219+
// Default values for the cobra help text
220+
kubeadmscheme.Scheme.Default(&flags.cfg)
221+
222+
cmd := &cobra.Command{
223+
Use: "check-expiration",
224+
Short: "Check certificates expiration for a Kubernetes cluster",
225+
Long: expirationLongDesc,
226+
Run: func(cmd *cobra.Command, args []string) {
227+
internalcfg, err := configutil.LoadOrDefaultInitConfiguration(flags.cfgPath, &flags.cfg)
228+
kubeadmutil.CheckErr(err)
229+
230+
// Get a renewal manager for the given cluster configuration
231+
rm, err := renewal.NewManager(&internalcfg.ClusterConfiguration, kdir)
232+
kubeadmutil.CheckErr(err)
233+
234+
// Get all the certificate expiration info
235+
yesNo := func(b bool) string {
236+
if b {
237+
return "yes"
238+
}
239+
return "no"
240+
}
241+
w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0)
242+
fmt.Fprintln(w, "CERTIFICATE\tEXPIRES\tRESIDUAL TIME\tEXTERNALLY MANAGED")
243+
for _, handler := range rm.Certificates() {
244+
e, err := rm.GetExpirationInfo(handler.Name)
245+
if err != nil {
246+
kubeadmutil.CheckErr(err)
247+
}
248+
249+
s := fmt.Sprintf("%s\t%s\t%s\t%-8v",
250+
e.Name,
251+
e.ExpirationDate.Format("Jan 02, 2006 15:04 MST"),
252+
duration.ShortHumanDuration(e.ResidualTime()),
253+
yesNo(e.ExternallyManaged),
254+
)
255+
256+
fmt.Fprintln(w, s)
257+
}
258+
w.Flush()
259+
},
260+
}
261+
addExpirationFlags(cmd, flags)
262+
263+
return cmd
264+
}
265+
266+
type expirationFlags struct {
267+
cfgPath string
268+
cfg kubeadmapiv1beta2.InitConfiguration
269+
}
270+
271+
func addExpirationFlags(cmd *cobra.Command, flags *expirationFlags) {
272+
options.AddConfigFlag(cmd.Flags(), &flags.cfgPath)
273+
options.AddCertificateDirFlag(cmd.Flags(), &flags.cfg.CertificatesDir)
274+
}

cmd/kubeadm/app/constants/constants.go

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ const (
4242
// should be joined with KubernetesDir.
4343
TempDirForKubeadm = "tmp"
4444

45+
// CertificateValidity defines the validity for all the signed certificates generated by kubeadm
46+
CertificateValidity = time.Hour * 24 * 365
47+
4548
// CACertAndKeyBaseName defines certificate authority base name
4649
CACertAndKeyBaseName = "ca"
4750
// CACertName defines certificate name

cmd/kubeadm/app/phases/certs/renewal/BUILD

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go_library(
44
name = "go_default_library",
55
srcs = [
66
"apirenewer.go",
7+
"expiration.go",
78
"filerenewer.go",
89
"manager.go",
910
"readwriter.go",
@@ -32,6 +33,7 @@ go_test(
3233
name = "go_default_test",
3334
srcs = [
3435
"apirenewer_test.go",
36+
"expiration_test.go",
3537
"filerenewer_test.go",
3638
"manager_test.go",
3739
"readwriter_test.go",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2019 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 renewal
18+
19+
import (
20+
"crypto/x509"
21+
"time"
22+
)
23+
24+
// ExpirationInfo defines expiration info for a certificate
25+
type ExpirationInfo struct {
26+
// Name of the certificate
27+
// For PKI certificates, it is the name defined in the certsphase package, while for certificates
28+
// embedded in the kubeConfig files, it is the kubeConfig file name defined in the kubeadm constants package.
29+
// If you use the CertificateRenewHandler returned by Certificates func, handler.Name already contains the right value.
30+
Name string
31+
32+
// ExpirationDate defines certificate expiration date
33+
ExpirationDate time.Time
34+
35+
// ExternallyManaged defines if the certificate is externally managed, that is when
36+
// the signing CA certificate is provided without the certificate key (In this case kubeadm can't renew the certificate)
37+
ExternallyManaged bool
38+
}
39+
40+
// newExpirationInfo returns a new ExpirationInfo
41+
func newExpirationInfo(name string, cert *x509.Certificate, externallyManaged bool) *ExpirationInfo {
42+
return &ExpirationInfo{
43+
Name: name,
44+
ExpirationDate: cert.NotAfter,
45+
ExternallyManaged: externallyManaged,
46+
}
47+
}
48+
49+
// ResidualTime returns the time missing to expiration
50+
func (e *ExpirationInfo) ResidualTime() time.Duration {
51+
return e.ExpirationDate.Sub(time.Now())
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
Copyright 2019 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 renewal
18+
19+
import (
20+
"crypto/x509"
21+
"math"
22+
"testing"
23+
"time"
24+
)
25+
26+
func TestExpirationInfo(t *testing.T) {
27+
validity := 365 * 24 * time.Hour
28+
cert := &x509.Certificate{
29+
NotAfter: time.Now().Add(validity),
30+
}
31+
32+
e := newExpirationInfo("x", cert, false)
33+
34+
if math.Abs(float64(validity-e.ResidualTime())) > float64(5*time.Second) { // using 5s of tolerance becase the function is not determinstic (it uses time.Now()) and we want to avoid flakes
35+
t.Errorf("expected IsInRenewalWindow equal to %v, saw %v", validity, e.ResidualTime())
36+
}
37+
}

cmd/kubeadm/app/phases/certs/renewal/manager.go

+53-12
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,14 @@ func (rm *Manager) RenewUsingLocalCA(name string) (bool, error) {
161161
return false, errors.Errorf("%s is not a valid certificate for this cluster", name)
162162
}
163163

164-
// checks if the we are in the external CA case (CA certificate provided without the certificate key)
165-
var externalCA bool
166-
switch handler.CABaseName {
167-
case kubeadmconstants.CACertAndKeyBaseName:
168-
externalCA, _ = certsphase.UsingExternalCA(rm.cfg)
169-
case kubeadmconstants.FrontProxyCACertAndKeyBaseName:
170-
externalCA, _ = certsphase.UsingExternalFrontProxyCA(rm.cfg)
171-
case kubeadmconstants.EtcdCACertAndKeyBaseName:
172-
externalCA = false
173-
default:
174-
return false, errors.Errorf("unknown certificate authority %s", handler.CABaseName)
164+
// checks if the certificate is externally managed (CA certificate provided without the certificate key)
165+
externallyManaged, err := rm.IsExternallyManaged(handler)
166+
if err != nil {
167+
return false, err
175168
}
176169

177170
// in case of external CA it is not possible to renew certificates, then return early
178-
if externalCA {
171+
if externallyManaged {
179172
return false, nil
180173
}
181174

@@ -275,6 +268,54 @@ func (rm *Manager) CreateRenewCSR(name, outdir string) error {
275268
return nil
276269
}
277270

271+
// GetExpirationInfo returns certificate expiration info.
272+
// For PKI certificates, use the name defined in the certsphase package, while for certificates
273+
// embedded in the kubeConfig files, use the kubeConfig file name defined in the kubeadm constants package.
274+
// If you use the CertificateRenewHandler returned by Certificates func, handler.Name already contains the right value.
275+
func (rm *Manager) GetExpirationInfo(name string) (*ExpirationInfo, error) {
276+
handler, ok := rm.certificates[name]
277+
if !ok {
278+
return nil, errors.Errorf("%s is not a known certificate", name)
279+
}
280+
281+
// checks if the certificate is externally managed (CA certificate provided without the certificate key)
282+
externallyManaged, err := rm.IsExternallyManaged(handler)
283+
if err != nil {
284+
return nil, err
285+
}
286+
287+
// reads the current certificate
288+
cert, err := handler.readwriter.Read()
289+
if err != nil {
290+
return nil, err
291+
}
292+
293+
// returns the certificate expiration info
294+
return newExpirationInfo(name, cert, externallyManaged), nil
295+
}
296+
297+
// IsExternallyManaged checks if we are in the external CA case (CA certificate provided without the certificate key)
298+
func (rm *Manager) IsExternallyManaged(h *CertificateRenewHandler) (bool, error) {
299+
switch h.CABaseName {
300+
case kubeadmconstants.CACertAndKeyBaseName:
301+
externallyManaged, err := certsphase.UsingExternalCA(rm.cfg)
302+
if err != nil {
303+
return false, errors.Wrapf(err, "Error checking external CA condition for %s certificate authority", h.CABaseName)
304+
}
305+
return externallyManaged, nil
306+
case kubeadmconstants.FrontProxyCACertAndKeyBaseName:
307+
externallyManaged, err := certsphase.UsingExternalFrontProxyCA(rm.cfg)
308+
if err != nil {
309+
return false, errors.Wrapf(err, "Error checking external CA condition for %s certificate authority", h.CABaseName)
310+
}
311+
return externallyManaged, nil
312+
case kubeadmconstants.EtcdCACertAndKeyBaseName:
313+
return false, nil
314+
default:
315+
return false, errors.Errorf("unknown certificate authority %s", h.CABaseName)
316+
}
317+
}
318+
278319
func certToConfig(cert *x509.Certificate) *certutil.Config {
279320
return &certutil.Config{
280321
CommonName: cert.Subject.CommonName,

0 commit comments

Comments
 (0)