Page MenuHomePhabricator

Kubernetes v1.23 use PKI for service-account signing (instead of cergen)
Closed, ResolvedPublic

Description

During T329633: Upgrade the aux-eqiad cluster to k8s 1.23 @elukey discovered calico and coredns services having issues authenticating against the kubernetes api.

From calico-typha:

Failed to determine migration requirements error=unable to query ClusterInformation to determine Calico version: connection is unauthorized: Unauthorized

From calico-node:

[WARNING][9] startup/startup.go 442: Connection to the datastore is unauthorized error=connection is unauthorized: Unauthorized

Some coredns where looping in:

[INFO] plugin/ready: Still waiting on: "kubernetes"

kube-apiserver was logging:

authentication.go:63] "Unable to authenticate the request" err="[invalid bearer token, square/go-jose: error in cryptographic primitive]"

The investigation revealed that with the switch to PKI certificates each control-plane node now has it's own private key to sign (in cluster) service account (jwt) tokens with (kube-apiserver parameter --service-account-signing-key-file, see command-line-tools-reference). As a consequence, these tokens can then only be validated via the matching public key e.g. only by the control-plane that has signed the token in first place. Requests to the other control-plane will be rejected with the above error.

Unfortunately it is not possible to use the intermediate CA to validate the tokens which means that all control-planes need access to the public keys of all other control-planes in order to be able to validate tokens (or have a shared private key ofc.). The only documentation regarding this I could come up with (unfortunately the best practice guide does not provide any details on multi control-plane setups) is https://v1-23.docs.kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/#manual-certs which suggests that the keys for the general and front-proxy intermediate need to be shared as well, which I don't think is required. They are also not referenced/used anywhere. What is referenced/used it the public key of both intermediates for validating requests (but that is obviously already shared).

In manual tests we provided both/all of the public keys for service account signing to each kube-apiserver (by specifying --service-account-key-file multiple times) which seemed to work but absolutely needs more testing.

The reason for this being discovered so late in the process is (besides me not realizing obviously ;)) that the wikikube staging clusters run a single master - which we should definitely change now. T329827

Problem

We need to use only one certificate for service account signing or we need to find a way to sync the public keys of all certificates used to all masters.

Workaround

The current workaround to be able to continue the cluster upgrades, especially wikikube-codfw next week, is to continue using the cergen generated certificate (which is shared between control-planes) for service-account signing for now (see attached patches). This is already in setup for the aux cluster (T329633).

Requirements
  • No manual certificate management (like with cergen) / Cert(s) should be generated by PKI
  • Refreshed certificate(s) should be rolled out automatically
  • Ensure that only not all kube-apiservers are restarted at the same time (for a cert refresh, as hot-reloading is not supported)
  • The process should not take to long (TBD). In case a certificate key changes, the new public key should be known to all kube-apiservers before a service account token is signed by the new key. This is an edge case though (for example then reimaging a control-plane) as keys don't usually change.
Solution 1: Store public keys as facts in puppet

Making the keys an exported resource is not possible because they are generated on the nodes filesystem via shellout so they never really "reach" puppet.

Have all control-planes generate a certificate and store the public key as fact puppet. All control-planes can then fetch the public keys on the next puppet run and restart kube-apiserver on change.
We would need to create a fact that stores the public key from a particular, hardcoded path like:

Facter.add(:kubernetes_service_account_signing_key) do
  confine { File.exists?("/etc/kubernetes/service_account_key.pem") }
  setcode do
    File.read("/etc/kubernetes/service_account_key.pem")
  end
end

Then puppetdb::query_facts and add them all to kube-apiserver.

New public keys would be made available after maximal 30 minutes (control-plane1 puppet run refreshes the cert and updates the fact, control-plane2 puppet run can download the cert).

Solution 2: Sync via kubernetes etcd and confd CHOSEN

Have all control-planes generate a certificate and make puppet on control-planes upload the public key to the kubernetes etcd (control-planes already depend on etcd). A confd setup on all control-planes could the pull all those public keys onto each control-plane and restart kube-apiserver (with etcd lock) if there has been a key change/update.

New public keys would be mad available almost immediately (depending on the random jitter).

Solution 3: Use only one cert

Have a different entity (cumin host for example) issue a certificate, copy private and public key to all control-planes (via ssh) and issue kube-apiserver restarts (with etcd lock).

Or add a cookbook that:

  • checks validity of the cert, if it's more than N days away from expiration, exit
  • if it's about to expire, disable puppet on all the k8s masters
  • regenerate the cert with the pki, commit it to puppet
  • reenable and run puppet on teh k8s masters, one at a time

New public keys would be mad available almost immediately.
Kubernetes clusters will depend on an external system to operate.

Orchestrate kube-apiserver restarts

To ensure that only one apiserver restarts at a given time, we could use an etcd lock (in the kubernetes clusters etcd):

etcdctl lock apiserver-restart systemctrl restart kube-apiserver

This is implemented using a second systemd unit kube-apiserver-safe-restart.service which is called by confd as well as notified by puppet on relevant changes. This means that kube-apiserver restarts issued by some automation will be coordinated.

Todo:

  • Configure all clusters to sign new tokens with their own PKI certs
  • Drop all service account token secrets from the API that are still signed by cergen certs
    • There is a list with the names of all deleted secrets in deploy1002:/home/jayme/kube-apiserver-sa/deleted_service_account_tokens.log
  • Configure all clusters to no longer trust cergen signed tokens
  • Remove cergen certs from (private) puppet, documentation etc

Details

SubjectRepoBranchLines +/-
operations/puppetproduction+0 -14
operations/puppetproduction+0 -150
labs/privatemaster+0 -7
operations/puppetproduction+0 -28
operations/puppetproduction+6 -64
operations/puppetproduction+1 -5
operations/puppetproduction+2 -4
operations/puppetproduction+18 -2
operations/puppetproduction+31 -21
operations/debs/kubernetesv1.23+12 -0
operations/puppetproduction+4 -1
operations/puppetproduction+29 -12
operations/deployment-chartsmaster+3 -7
operations/deployment-chartsmaster+4 -0
operations/docker-images/production-imagesmaster+12 -3
operations/software/heptiolabs/eventrouterv0.4-wmf+407 K -235 K
operations/puppetproduction+1 -1
operations/puppetproduction+4 -1
operations/puppetproduction+16 -18
operations/puppetproduction+1 -0
operations/dnsmaster+27 -0
operations/puppetproduction+20 -0
operations/puppetproduction+13 -0
operations/puppetproduction+28 -2
operations/puppetproduction+84 -84
operations/puppetproduction+2 -2
operations/puppetproduction+16 -3
operations/puppetproduction+2 -1
operations/puppetproduction+40 -16
Show related patches Customize query in gerrit

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
JMeybohm updated the task description. (Show Details)

Change 936666 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] k8s::apiserver: Implement kube-apiserver reload

https://gerrit.wikimedia.org/r/936666

Change 936666 merged by JMeybohm:

[operations/puppet@production] k8s::apiserver: Implement kube-apiserver reload

https://gerrit.wikimedia.org/r/936666

Change 937434 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: admin.conf on control-plane should use the local api

https://gerrit.wikimedia.org/r/937434

Change 937441 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] cfssl::cert: Add support for notifying multiple services

https://gerrit.wikimedia.org/r/937441

Change 937442 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Publish service-account cert to etcd

https://gerrit.wikimedia.org/r/937442

Change 937793 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes: Add etcd srv names to clusterconfig structure

https://gerrit.wikimedia.org/r/937793

Change 937441 merged by JMeybohm:

[operations/puppet@production] cfssl::cert: Add support for notifying multiple services

https://gerrit.wikimedia.org/r/937441

Change 937434 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: admin.conf on control-plane should use the local api

https://gerrit.wikimedia.org/r/937434

Change 939630 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Add confd config writing all sa certs

https://gerrit.wikimedia.org/r/939630

Change 937442 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Publish service-account cert to etcd

https://gerrit.wikimedia.org/r/937442

Change 937793 merged by JMeybohm:

[operations/puppet@production] kubernetes: Add etcd srv names to clusterconfig structure

https://gerrit.wikimedia.org/r/937793

Change 939630 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Add confd config writing all sa certs

https://gerrit.wikimedia.org/r/939630

Change 940334 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/dns@master] Add _etcd-client-ssl._tcp SRV records for k8s etcd clusters

https://gerrit.wikimedia.org/r/940334

Change 940334 merged by JMeybohm:

[operations/dns@master] Add _etcd-client-ssl._tcp SRV records for k8s etcd clusters

https://gerrit.wikimedia.org/r/940334

Change 940339 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] Add dummy etcd_srv_name to kubernetes::clusetrs in CI

https://gerrit.wikimedia.org/r/940339

Change 940339 merged by JMeybohm:

[operations/puppet@production] Add dummy etcd_srv_name to kubernetes::clusetrs in CI

https://gerrit.wikimedia.org/r/940339

Change 940350 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] confd::instance: Allow to specify multiple backend hosts

https://gerrit.wikimedia.org/r/940350

Change 940350 merged by JMeybohm:

[operations/puppet@production] confd::instance: Allow to specify multiple backend hosts

https://gerrit.wikimedia.org/r/940350

Change 940357 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: etcd_servers list being created to late

https://gerrit.wikimedia.org/r/940357

Change 940357 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: etcd_servers list being created to late

https://gerrit.wikimedia.org/r/940357

Change 940369 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Fix confd template as keys start with /

https://gerrit.wikimedia.org/r/940369

Change 940369 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Fix confd template as keys start with /

https://gerrit.wikimedia.org/r/940369

Change 954677 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Validate SA tokens with the certs of all masters

https://gerrit.wikimedia.org/r/954677

I put together a small go tool to validate some/all tokens with provided certficates (one or many). I did not see any other way of checking which token is signed by which key - and we need to make sure all tokens are signed by a pki key before we remove the cergen cert from the list for validation. For anybody interested, the public certs of all clusters (cergen and pki) can be found at deploy1002:/home/jayme/kube-apiserver-sa/certs/ (a compiled version of the below is at deploy1002:/home/jayme/kube-apiserver-sa/k8s-jwt-validator

1package main
2
3import (
4 "bufio"
5 "context"
6 "flag"
7 "fmt"
8 "log"
9 "os"
10
11 "crypto/x509"
12 "encoding/pem"
13
14 jwt "github.com/golang-jwt/jwt/v5"
15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16 "k8s.io/client-go/kubernetes"
17 "k8s.io/client-go/tools/clientcmd"
18)
19
20var (
21 kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file (optional)")
22)
23
24func loadX509Cert(x509CertificatePath string) (*x509.Certificate, error) {
25 certData, err := os.ReadFile(x509CertificatePath)
26 if err != nil {
27 return nil, err
28 }
29
30 block, _ := pem.Decode(certData)
31 if block == nil {
32 return nil, fmt.Errorf("failed to parse certificate PEM")
33 }
34
35 cert, err := x509.ParseCertificate(block.Bytes)
36 if err != nil {
37 return nil, err
38 }
39
40 return cert, nil
41}
42
43func parseJWT(jwtToken string, publicKey any) (*jwt.Token, error) {
44 token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
45 return publicKey, nil
46 })
47 if err != nil {
48 return nil, err
49 }
50
51 return token, nil
52}
53
54func main() {
55 tokensToValidate := make(map[string]string)
56
57 flag.Parse()
58 x509CertPaths := flag.Args()
59 if len(x509CertPaths) <= 0 {
60 log.Fatalln("No x.509 certificate for validation provided")
61 }
62 x509Certs := make([]*x509.Certificate, len(x509CertPaths))
63 for idx, certPath := range x509CertPaths {
64 cert, err := loadX509Cert(certPath)
65 if err != nil {
66 log.Fatalf("Error loading X.509 certificate %s: %v", certPath, err)
67 }
68 x509Certs[idx] = cert
69 }
70
71 if *kubeconfig != "" {
72 // Fetch secrets from k8s if kubeconfig is provided
73 config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
74 if err != nil {
75 panic(err)
76 }
77 clientset, err := kubernetes.NewForConfig(config)
78 if err != nil {
79 panic(err)
80 }
81 // List all secrets of type "kubernetes.io/service-account-token"
82 secrets, err := clientset.CoreV1().Secrets("").List(context.Background(), metav1.ListOptions{
83 FieldSelector: "type=kubernetes.io/service-account-token",
84 })
85 if err != nil {
86 log.Fatalf("Error listing secrets: %v", err)
87 }
88
89 for _, secret := range secrets.Items {
90 name := fmt.Sprintf("%s/%s", secret.Namespace, secret.Name)
91 token, found := secret.Data["token"]
92 if !found {
93 log.Fatalf("Token field not found in the secret %s", name)
94 }
95 tokensToValidate[name] = string(token)
96 }
97 } else {
98 // Without kubeconfig, expect a token from stdin
99 stdinScanner := bufio.NewScanner(os.Stdin)
100 stdinScanner.Scan()
101 tokensToValidate["stdin"] = stdinScanner.Text()
102 }
103
104 var validationError error
105 var anyValidationFailed bool
106 for tokenName, tokenString := range tokensToValidate {
107 for certIdx, cert := range x509Certs {
108 _, validationError = parseJWT(tokenString, cert.PublicKey)
109 if validationError == nil {
110 fmt.Printf("%s validated with: %s\n", tokenName, x509CertPaths[certIdx])
111 break
112 }
113 }
114 if validationError != nil {
115 anyValidationFailed = true
116 fmt.Printf("%s validation FAILED: %v\n", tokenName, validationError)
117 }
118 }
119
120 if anyValidationFailed {
121 os.Exit(1)
122 }
123}

Change 955748 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/software/heptiolabs/eventrouter@v0.4-wmf] Update to kubernetes client-go 0.23.14

https://gerrit.wikimedia.org/r/955748

Change 955750 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/docker-images/production-images@master] eventrouter: Bump k8s client-go to v0.23.14

https://gerrit.wikimedia.org/r/955750

Change 955753 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/deployment-charts@master] admin_ng: Deploy eventrouter 0.4.0-1 to wikikube staging

https://gerrit.wikimedia.org/r/955753

Change 955748 merged by JMeybohm:

[operations/software/heptiolabs/eventrouter@v0.4-wmf] Update to kubernetes client-go 0.23.14

https://gerrit.wikimedia.org/r/955748

Change 955750 merged by JMeybohm:

[operations/docker-images/production-images@master] eventrouter: Bump k8s client-go to v0.23.14

https://gerrit.wikimedia.org/r/955750

Change 955753 merged by jenkins-bot:

[operations/deployment-charts@master] admin_ng: Deploy eventrouter 0.4.0-1 to wikikube staging

https://gerrit.wikimedia.org/r/955753

Change 955863 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/deployment-charts@master] eventrouter: Update to 0.4.0-1

https://gerrit.wikimedia.org/r/955863

Change 955863 merged by jenkins-bot:

[operations/deployment-charts@master] eventrouter: Update to 0.4.0-1

https://gerrit.wikimedia.org/r/955863

Change 954677 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Validate SA tokens with the certs of all masters

https://gerrit.wikimedia.org/r/954677

Change 956414 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Switch staging to use PKI for SA signing

https://gerrit.wikimedia.org/r/956414

Change 956414 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Switch staging to use PKI for SA signing

https://gerrit.wikimedia.org/r/956414

Change 956449 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Switch staging to use PKI for SA signing

https://gerrit.wikimedia.org/r/956449

Change 956842 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] k8s::apiserver: Use a separate systemd service for safe restarts

https://gerrit.wikimedia.org/r/956842

Change 956889 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: control-plane components should use the local api

https://gerrit.wikimedia.org/r/956889

Change 956900 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/debs/kubernetes@v1.23] Add systemd dependencies to kube-apiserver

https://gerrit.wikimedia.org/r/956900

Change 956900 merged by JMeybohm:

[operations/debs/kubernetes@v1.23] Add systemd dependencies to kube-apiserver

https://gerrit.wikimedia.org/r/956900

Mentioned in SAL (#wikimedia-operations) [2023-09-13T10:49:41Z] <jayme> imported kubernetes_1.23.14-3 to bullseye-wikimedia component/kubernetes123 - T329826

Mentioned in SAL (#wikimedia-operations) [2023-09-13T14:51:27Z] <jayme> updated kubernetes-* packages fleet wide to 1.23.14-3 - T329826

Change 956842 merged by JMeybohm:

[operations/puppet@production] k8s::apiserver: Use a separate systemd service for safe restarts

https://gerrit.wikimedia.org/r/956842

Change 956889 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: control-plane components should use the local api

https://gerrit.wikimedia.org/r/956889

Change 956449 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Switch to PKI for SA signing

https://gerrit.wikimedia.org/r/956449

Change 958404 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Switch to PKI for SA signing

https://gerrit.wikimedia.org/r/958404

Change 958405 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Remove the use of cergen certs from apiserver

https://gerrit.wikimedia.org/r/958405

Change 958426 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] kubernetes::master: Cleanup absent cergen resource

https://gerrit.wikimedia.org/r/958426

Change 958404 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Switch to PKI for SA signing

https://gerrit.wikimedia.org/r/958404

Mentioned in SAL (#wikimedia-operations) [2023-09-18T11:44:41Z] <jayme> removed cergen certs from the list of trusted service account token signers on all kubernetes clusters - T329826

Change 958405 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Remove the use of cergen certs from apiserver

https://gerrit.wikimedia.org/r/958405

Change 958426 merged by JMeybohm:

[operations/puppet@production] kubernetes::master: Cleanup absent cergen resource

https://gerrit.wikimedia.org/r/958426

Change 958898 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[labs/private@master] Drop kubernetes cergen certs

https://gerrit.wikimedia.org/r/958898

Change 958898 merged by JMeybohm:

[labs/private@master] Drop kubernetes cergen certs

https://gerrit.wikimedia.org/r/958898

Removed all the certs with [puppet-private] (23d9433a) and ran puppet on all masters without issue. Wikitech has been updated as well to remove all mentions of cergen.

Change 979906 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] Drop remaining k8s master cergen certs

https://gerrit.wikimedia.org/r/979906

Change 979906 merged by JMeybohm:

[operations/puppet@production] Drop remaining k8s master cergen certs

https://gerrit.wikimedia.org/r/979906

Change #1024406 had a related patch set uploaded (by JMeybohm; author: JMeybohm):

[operations/puppet@production] Kubernetes: Drop unused etcd_srv_name

https://gerrit.wikimedia.org/r/1024406

Change #1024406 merged by JMeybohm:

[operations/puppet@production] Kubernetes: Drop unused etcd_srv_name

https://gerrit.wikimedia.org/r/1024406