github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/security/certmanager/certmanager_security.go (about)

     1  // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package certmanagersec
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/rand"
    11  	"crypto/rsa"
    12  	"crypto/tls"
    13  	"crypto/x509"
    14  	"crypto/x509/pkix"
    15  	"encoding/asn1"
    16  	"encoding/base64"
    17  	"encoding/json"
    18  	"encoding/pem"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	"os"
    23  	"path/filepath"
    24  	"time"
    25  
    26  	"github.com/choria-io/go-choria/backoff"
    27  	"github.com/choria-io/go-choria/inter"
    28  	"github.com/choria-io/go-choria/internal/util"
    29  	"github.com/choria-io/go-choria/providers/security/filesec"
    30  	"github.com/sirupsen/logrus"
    31  	"github.com/tidwall/gjson"
    32  )
    33  
    34  // CertManagerSecurity implements a security provider that auto enrolls with Kubernetes Cert Manager
    35  //
    36  // It only supports being used inside a cluster and does not use the kubernetes API client libraries
    37  // due to dependencies and just awfulness with go mod
    38  type CertManagerSecurity struct {
    39  	conf *Config
    40  	log  *logrus.Entry
    41  
    42  	ctx  context.Context
    43  	fsec *filesec.FileSecurity
    44  }
    45  
    46  type Config struct {
    47  	apiVersion      string
    48  	altnames        []string
    49  	namespace       string
    50  	issuer          string
    51  	identity        string
    52  	replace         bool
    53  	sslDir          string
    54  	privilegedUsers []string
    55  	csr             string
    56  	cert            string
    57  	key             string
    58  	ca              string
    59  	legacyCerts     bool
    60  }
    61  
    62  func New(opts ...Option) (*CertManagerSecurity, error) {
    63  	cm := &CertManagerSecurity{}
    64  
    65  	for _, opt := range opts {
    66  		err := opt(cm)
    67  		if err != nil {
    68  			return nil, err
    69  		}
    70  	}
    71  
    72  	if cm.conf == nil {
    73  		return nil, fmt.Errorf("configuration not given")
    74  	}
    75  
    76  	if cm.log == nil {
    77  		return nil, fmt.Errorf("logger not given")
    78  	}
    79  
    80  	if cm.ctx == nil {
    81  		return nil, fmt.Errorf("context is required")
    82  	}
    83  
    84  	cm.conf.csr = filepath.Join(cm.conf.sslDir, "csr.pem")
    85  	cm.conf.cert = filepath.Join(cm.conf.sslDir, "cert.pem")
    86  	cm.conf.key = filepath.Join(cm.conf.sslDir, "key.pem")
    87  	cm.conf.ca = filepath.Join(cm.conf.sslDir, "ca.pem")
    88  
    89  	return cm, cm.reinit()
    90  }
    91  
    92  func (cm *CertManagerSecurity) reinit() error {
    93  	var err error
    94  
    95  	fc := filesec.Config{
    96  		Identity:                   cm.conf.identity,
    97  		Certificate:                cm.conf.cert,
    98  		Key:                        cm.conf.key,
    99  		CA:                         cm.conf.ca,
   100  		PrivilegedUsers:            cm.conf.privilegedUsers,
   101  		BackwardCompatVerification: cm.conf.legacyCerts,
   102  	}
   103  
   104  	cm.fsec, err = filesec.New(filesec.WithConfig(&fc), filesec.WithLog(cm.log))
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	if cm.shouldEnroll() {
   110  		cm.log.Infof("Attempting to enroll with Cert Manager in namespace %q using issuer %q", cm.conf.namespace, cm.conf.issuer)
   111  		err = cm.Enroll(cm.ctx, time.Minute, func(_ string, i int) {
   112  			cm.log.Infof("Enrollment attempt %d", i)
   113  		})
   114  		if err != nil {
   115  			return fmt.Errorf("enrollment failed: %s", err)
   116  		}
   117  
   118  		cm.log.Infof("Enrollment with Cert Manager completed in namespace %q", cm.conf.namespace)
   119  	}
   120  
   121  	return nil
   122  }
   123  
   124  func (cm *CertManagerSecurity) Enroll(ctx context.Context, wait time.Duration, cb func(digest string, try int)) error {
   125  	if !cm.shouldEnroll() {
   126  		cm.log.Infof("Enrollment already completed, remove %q to force re-enrolment", cm.conf.sslDir)
   127  		return nil
   128  	}
   129  
   130  	err := cm.createSSLDirectories()
   131  	if err != nil {
   132  		return fmt.Errorf("could not initialize ssl directories: %s", err)
   133  	}
   134  
   135  	var key *rsa.PrivateKey
   136  	if !cm.privateKeyExists() {
   137  		cm.log.Debugf("Creating a new Private Key %s", cm.Identity())
   138  
   139  		key, err = cm.writePrivateKey()
   140  		if err != nil {
   141  			return fmt.Errorf("could not write a new private key: %s", err)
   142  		}
   143  	}
   144  
   145  	if !cm.csrExists() {
   146  		cm.log.Debugf("Creating a new CSR for %s", cm.Identity())
   147  
   148  		err = cm.writeCSR(key, cm.Identity(), "choria.io")
   149  		if err != nil {
   150  			return fmt.Errorf("could not write CSR: %s", err)
   151  		}
   152  	}
   153  
   154  	if !cm.publicCertExists() || cm.conf.replace {
   155  		err = cm.processCSR()
   156  		if err != nil {
   157  			return fmt.Errorf("csr submission failed: %s", err)
   158  		}
   159  	}
   160  
   161  	ctx, cancel := context.WithTimeout(ctx, time.Minute)
   162  	defer cancel()
   163  
   164  	err = backoff.Default.For(ctx, func(try int) error {
   165  		cm.log.Infof("Attempt %d at fetching certificate %q", try, cm.Identity())
   166  		return cm.fetchCertAndCA()
   167  	})
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  func (cm *CertManagerSecurity) BackingTechnology() inter.SecurityTechnology {
   176  	return cm.fsec.BackingTechnology()
   177  }
   178  
   179  func (cm *CertManagerSecurity) Provider() string {
   180  	return "certmanager"
   181  }
   182  
   183  func (cm *CertManagerSecurity) fetchCertAndCA() error {
   184  	url := fmt.Sprintf("https://kubernetes.default.svc/apis/cert-manager.io/%s/namespaces/%s/certificaterequests/%s", cm.conf.apiVersion, cm.conf.namespace, cm.Identity())
   185  	resp, err := cm.k8sRequest("GET", url, nil)
   186  	if err != nil {
   187  		return fmt.Errorf("could not load CSR for %q: %s", cm.Identity(), err)
   188  	}
   189  	defer resp.Body.Close()
   190  
   191  	body, err := io.ReadAll(resp.Body)
   192  	if err != nil {
   193  		return fmt.Errorf("could not load CSR for %q: %s", cm.Identity(), err)
   194  	}
   195  
   196  	if resp.StatusCode != 200 {
   197  		return fmt.Errorf("could not load CSR for %q: code: %d body: %q", cm.Identity(), resp.StatusCode, body)
   198  	}
   199  
   200  	ca := gjson.GetBytes(body, "status.ca")
   201  	if !ca.Exists() {
   202  		return fmt.Errorf("did not receive a CA from Cert Manager")
   203  	}
   204  
   205  	capem, err := base64.StdEncoding.DecodeString(ca.String())
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	err = os.WriteFile(cm.conf.ca, capem, 0644)
   211  	if err != nil {
   212  		return fmt.Errorf("could not write ca %s: %s", cm.conf.ca, err)
   213  	}
   214  
   215  	cert := gjson.GetBytes(body, "status.certificate")
   216  	if !cert.Exists() {
   217  		return fmt.Errorf("did not receive a certificate from Cert Manager")
   218  	}
   219  
   220  	certpem, err := base64.StdEncoding.DecodeString(cert.String())
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	err = os.WriteFile(cm.conf.cert, certpem, 0644)
   226  	if err != nil {
   227  		return fmt.Errorf("could not write certificate %s: %s", cm.conf.ca, err)
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func (cm *CertManagerSecurity) processCSR() error {
   234  	code, body, err := cm.submitCSR()
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	switch code {
   240  	case 201:
   241  		// ok
   242  	case 409:
   243  		if !cm.conf.replace {
   244  			return fmt.Errorf("found an existing CSR for %q", cm.Identity())
   245  		}
   246  
   247  		cm.log.Warnf("Found an existing CSR for %q, removing and creating a new one", cm.Identity())
   248  		code, err := cm.deleteCSR()
   249  		if err != nil {
   250  			return fmt.Errorf("deleting existing CSR for %q failed: %s", cm.Identity(), err)
   251  		}
   252  
   253  		if code != 200 {
   254  			return fmt.Errorf("deleting existing CSR for %q failed: code %d", cm.Identity(), code)
   255  		}
   256  
   257  		code, body, err = cm.submitCSR()
   258  		if err != nil {
   259  			return fmt.Errorf("csr creation failed: %s", err)
   260  		}
   261  
   262  		if code != 201 {
   263  			return fmt.Errorf("csr creation failed: code: %d body: %q", code, body)
   264  		}
   265  
   266  	default:
   267  		return fmt.Errorf("unexpected error from the Kubernetes API: code: %d body: %q", code, body)
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func (cm *CertManagerSecurity) deleteCSR() (code int, err error) {
   274  	url := fmt.Sprintf("https://kubernetes.default.svc/apis/cert-manager.io/%s/namespaces/%s/certificaterequests/%s", cm.conf.apiVersion, cm.conf.namespace, cm.Identity())
   275  	resp, err := cm.k8sRequest("DELETE", url, nil)
   276  	if err != nil {
   277  		return 500, err
   278  	}
   279  
   280  	return resp.StatusCode, nil
   281  }
   282  
   283  func (cm *CertManagerSecurity) submitCSR() (code int, body []byte, err error) {
   284  	csr, err := cm.csrTXT()
   285  	if err != nil {
   286  		return 500, nil, fmt.Errorf("could not read CSR: %s", err)
   287  	}
   288  
   289  	csrReq := map[string]any{
   290  		"apiVersion": fmt.Sprintf("cert-manager.io/%s", cm.conf.apiVersion),
   291  		"kind":       "CertificateRequest",
   292  		"metadata": map[string]any{
   293  			"name":      cm.Identity(),
   294  			"namespace": cm.conf.namespace,
   295  		},
   296  		"spec": map[string]any{
   297  			"issuerRef": map[string]any{
   298  				"name": cm.conf.issuer,
   299  			},
   300  			"csr":     csr,
   301  			"request": csr,
   302  		},
   303  	}
   304  
   305  	jreq, err := json.Marshal(csrReq)
   306  	if err != nil {
   307  		return 500, nil, err
   308  	}
   309  
   310  	cm.log.Infof("Submitting CSR for %q to Cert Manager", cm.Identity())
   311  
   312  	url := fmt.Sprintf("https://kubernetes.default.svc/apis/cert-manager.io/%s/namespaces/%s/certificaterequests", cm.conf.apiVersion, cm.conf.namespace)
   313  	resp, err := cm.k8sRequest("POST", url, bytes.NewReader(jreq))
   314  	if err != nil {
   315  		return 500, nil, err
   316  	}
   317  	defer resp.Body.Close()
   318  
   319  	body, err = io.ReadAll(resp.Body)
   320  	return resp.StatusCode, body, err
   321  }
   322  
   323  func (cm *CertManagerSecurity) k8sRequest(method string, url string, body io.Reader) (*http.Response, error) {
   324  	token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  
   329  	tlsConfig, err := cm.k8sTLSConfig()
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  
   334  	req, err := http.NewRequestWithContext(cm.ctx, method, url, body)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	req.Header.Set("Authorization", "Bearer "+string(token))
   340  	req.Header.Set("Content-Type", "application/json")
   341  	req.Header.Set("Accept", "application/json")
   342  
   343  	client := &http.Client{
   344  		Transport: &http.Transport{TLSClientConfig: tlsConfig},
   345  	}
   346  
   347  	return client.Do(req)
   348  }
   349  
   350  func (cm *CertManagerSecurity) k8sTLSConfig() (*tls.Config, error) {
   351  	tlsc := &tls.Config{
   352  		MinVersion:               tls.VersionTLS12,
   353  		PreferServerCipherSuites: true,
   354  	}
   355  
   356  	ca, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	pool := x509.NewCertPool()
   362  	if !pool.AppendCertsFromPEM(ca) {
   363  		return nil, fmt.Errorf("could not add kubernetes CA to the cert pool")
   364  	}
   365  
   366  	tlsc.ClientCAs = pool
   367  	tlsc.RootCAs = pool
   368  
   369  	return tlsc, nil
   370  }
   371  
   372  func (cm *CertManagerSecurity) csrTXT() ([]byte, error) {
   373  	return os.ReadFile(cm.conf.csr)
   374  }
   375  
   376  func (cm *CertManagerSecurity) shouldEnroll() bool {
   377  	// TODO re-enroll when expired
   378  	return !(cm.privateKeyExists() && cm.caExists() && cm.publicCertExists())
   379  }
   380  
   381  func (cm *CertManagerSecurity) writePrivateKey() (*rsa.PrivateKey, error) {
   382  	if cm.privateKeyExists() {
   383  		return nil, fmt.Errorf("a private key already exist for %s", cm.Identity())
   384  	}
   385  
   386  	key, err := rsa.GenerateKey(rand.Reader, 2048)
   387  	if err != nil {
   388  		return nil, fmt.Errorf("could not generate rsa key: %cm", err)
   389  	}
   390  
   391  	pemdata := pem.EncodeToMemory(
   392  		&pem.Block{
   393  			Type:  "RSA PRIVATE KEY",
   394  			Bytes: x509.MarshalPKCS1PrivateKey(key),
   395  		},
   396  	)
   397  
   398  	err = os.WriteFile(cm.conf.key, pemdata, 0640)
   399  	if err != nil {
   400  		return nil, fmt.Errorf("could not write private key: %cm", err)
   401  	}
   402  
   403  	return key, nil
   404  }
   405  
   406  func (cm *CertManagerSecurity) writeCSR(key *rsa.PrivateKey, cn string, ou string) error {
   407  	if cm.csrExists() {
   408  		return fmt.Errorf("a certificate request already exist for %s", cm.Identity())
   409  	}
   410  
   411  	path := cm.conf.csr
   412  
   413  	subj := pkix.Name{
   414  		CommonName:         cn,
   415  		OrganizationalUnit: []string{ou},
   416  	}
   417  
   418  	asn1Subj, err := asn1.Marshal(subj.ToRDNSequence())
   419  	if err != nil {
   420  		return fmt.Errorf("could not create subject: %s", err)
   421  	}
   422  
   423  	template := x509.CertificateRequest{
   424  		RawSubject:         asn1Subj,
   425  		SignatureAlgorithm: x509.SHA256WithRSA,
   426  	}
   427  
   428  	template.DNSNames = append(template.DNSNames, cm.conf.altnames...)
   429  
   430  	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, key)
   431  	if err != nil {
   432  		return fmt.Errorf("could not create csr: %s", err)
   433  	}
   434  
   435  	csr, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0640)
   436  	if err != nil {
   437  		return fmt.Errorf("could not open csr %s for writing: %s", path, err)
   438  	}
   439  	defer csr.Close()
   440  
   441  	err = pem.Encode(csr, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
   442  	if err != nil {
   443  		return fmt.Errorf("could not encode csr into %s: %s", path, err)
   444  	}
   445  
   446  	return nil
   447  }
   448  
   449  func (cm *CertManagerSecurity) createSSLDirectories() error {
   450  	err := os.MkdirAll(cm.conf.sslDir, 0771)
   451  	if err != nil {
   452  		return err
   453  	}
   454  
   455  	return nil
   456  }
   457  
   458  func (cm *CertManagerSecurity) csrExists() bool {
   459  	return util.FileExist(cm.conf.csr)
   460  }
   461  
   462  func (cm *CertManagerSecurity) privateKeyExists() bool {
   463  	return util.FileExist(cm.conf.key)
   464  }
   465  
   466  func (cm *CertManagerSecurity) publicCertExists() bool {
   467  	return util.FileExist(cm.conf.cert)
   468  }
   469  
   470  func (cm *CertManagerSecurity) caExists() bool {
   471  	return util.FileExist(cm.conf.ca)
   472  }
   473  
   474  func (cm *CertManagerSecurity) Validate() (errs []string, ok bool) {
   475  	if !util.FileIsDir(cm.conf.sslDir) {
   476  		errs = append(errs, fmt.Sprintf("%s does not exist or is not a directory", cm.conf.sslDir))
   477  	}
   478  
   479  	return errs, len(errs) == 0
   480  }
   481  
   482  func (cm *CertManagerSecurity) Identity() string {
   483  	return cm.conf.identity
   484  }
   485  
   486  func (cm *CertManagerSecurity) CallerName() string {
   487  	return cm.fsec.CallerName()
   488  }
   489  
   490  func (cm *CertManagerSecurity) CallerIdentity(caller string) (string, error) {
   491  	return cm.fsec.CallerIdentity(caller)
   492  }
   493  
   494  func (cm *CertManagerSecurity) SignBytes(b []byte) (signature []byte, err error) {
   495  	return cm.fsec.SignBytes(b)
   496  }
   497  
   498  func (cm *CertManagerSecurity) VerifySignatureBytes(dat []byte, sig []byte, public ...[]byte) (should bool, signer string) {
   499  	return cm.fsec.VerifySignatureBytes(dat, sig, public...)
   500  }
   501  
   502  func (cm *CertManagerSecurity) RemoteSignRequest(ctx context.Context, str []byte) (signed []byte, err error) {
   503  	return cm.fsec.RemoteSignRequest(ctx, str)
   504  }
   505  
   506  func (cm *CertManagerSecurity) IsRemoteSigning() bool {
   507  	return cm.fsec.IsRemoteSigning()
   508  }
   509  
   510  func (cm *CertManagerSecurity) ChecksumBytes(data []byte) []byte {
   511  	return cm.fsec.ChecksumBytes(data)
   512  }
   513  
   514  func (cm *CertManagerSecurity) ClientTLSConfig() (*tls.Config, error) {
   515  	return cm.fsec.ClientTLSConfig()
   516  }
   517  
   518  func (cm *CertManagerSecurity) TLSConfig() (*tls.Config, error) {
   519  	return cm.fsec.TLSConfig()
   520  }
   521  
   522  func (cm *CertManagerSecurity) SSLContext() (*http.Transport, error) {
   523  	return cm.fsec.SSLContext()
   524  }
   525  
   526  func (cm *CertManagerSecurity) HTTPClient(secure bool) (*http.Client, error) {
   527  	return cm.fsec.HTTPClient(secure)
   528  }
   529  
   530  func (cm *CertManagerSecurity) VerifyCertificate(certpem []byte, identity string) error {
   531  	return cm.fsec.VerifyCertificate(certpem, identity)
   532  }
   533  
   534  func (cm *CertManagerSecurity) PublicCert() (*x509.Certificate, error) {
   535  	return cm.fsec.PublicCert()
   536  }
   537  
   538  func (cm *CertManagerSecurity) PublicCertBytes() ([]byte, error) {
   539  	return cm.fsec.PublicCertBytes()
   540  }
   541  
   542  func (cm *CertManagerSecurity) ShouldAllowCaller(name string, callers ...[]byte) (privileged bool, err error) {
   543  	return cm.fsec.ShouldAllowCaller(name, callers...)
   544  }
   545  
   546  func (cm *CertManagerSecurity) TokenBytes() ([]byte, error) {
   547  	return nil, fmt.Errorf("tokens not available for certmanager security provider")
   548  }
   549  
   550  func (cm *CertManagerSecurity) ShouldSignReplies() bool { return false }