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 }