github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/security/puppetsec/puppet_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 puppetsec provides a Puppet compatable Security Provider 6 // 7 // The provider supports enrolling into a Puppet CA by creating a 8 // key and csr, sending it to the PuppetCA and waiting for it to 9 // be signed and later it will download the certificate once signed 10 package puppetsec 11 12 import ( 13 "bytes" 14 "context" 15 "crypto/rand" 16 "crypto/rsa" 17 "crypto/sha256" 18 "crypto/tls" 19 "crypto/x509" 20 "crypto/x509/pkix" 21 "encoding/asn1" 22 "encoding/pem" 23 "errors" 24 "fmt" 25 "io" 26 "net/http" 27 "os" 28 "path/filepath" 29 "time" 30 31 "github.com/choria-io/go-choria/inter" 32 "github.com/choria-io/go-choria/internal/util" 33 "github.com/choria-io/go-choria/tlssetup" 34 35 "github.com/choria-io/go-choria/providers/security/filesec" 36 "github.com/choria-io/go-choria/srvcache" 37 "github.com/sirupsen/logrus" 38 ) 39 40 // Resolver provides DNS lookup facilities 41 type Resolver interface { 42 QuerySrvRecords(records []string) (srvcache.Servers, error) 43 } 44 45 // BuildInfoProvider provides info about the build 46 type BuildInfoProvider interface { 47 ClientIdentitySuffix() string 48 } 49 50 // PuppetSecurity implements SecurityProvider reusing AIO Puppet settings 51 // it supports enrollment the same way `puppet agent --waitforcert 10` does 52 type PuppetSecurity struct { 53 res Resolver 54 conf *Config 55 log *logrus.Entry 56 57 fsec *filesec.FileSecurity 58 } 59 60 // Config is the configuration for PuppetSecurity 61 type Config struct { 62 // Identity when not empty will force the identity to be used for validations etc 63 Identity string 64 65 // SSLDir is the directory where Puppet stores it's SSL 66 SSLDir string 67 68 // PrivilegedUsers is a list of regular expressions that identity privilged users 69 PrivilegedUsers []string 70 71 // AllowList is a list of regular expressions that identity valid users to allow in 72 AllowList []string 73 74 // DisableTLSVerify disables TLS verify in HTTP clients etc 75 DisableTLSVerify bool 76 77 // PuppetCAHost is the hostname of the PuppetCA 78 PuppetCAHost string 79 80 // PuppetCAPort is the port of the PuppetCA 81 PuppetCAPort int 82 83 // DisableSRV prevents SRV lookups 84 DisableSRV bool 85 86 // Is a URL where a remote signer is running 87 RemoteSignerURL string 88 89 // RemoteSignerTokenFile is a file with a token for access to the remote signer 90 RemoteSignerTokenFile string 91 92 // RemoteSignerTokenEnvironment is an environment variable that will hold the signer token 93 RemoteSignerTokenEnvironment string 94 95 useFakeUID bool 96 fakeUID int 97 98 // TLSConfig is the shared TLS configuration 99 TLSConfig *tlssetup.Config 100 101 // AltNames are additional names to add to the CSR 102 AltNames []string 103 104 // IdentitySuffix is the suffix to append to user names when creating certnames and identities 105 IdentitySuffix string 106 107 // RemoteSigner is the signer used to sign requests using a remote like AAA Service 108 RemoteSigner inter.RequestSigner 109 } 110 111 // New creates a new instance of the Puppet Security Provider 112 func New(opts ...Option) (*PuppetSecurity, error) { 113 p := &PuppetSecurity{} 114 115 for _, opt := range opts { 116 err := opt(p) 117 if err != nil { 118 return nil, err 119 } 120 } 121 122 if p.conf == nil { 123 return nil, errors.New("configuration not given") 124 } 125 126 if p.log == nil { 127 return nil, errors.New("logger not given") 128 } 129 130 if p.conf.Identity == "" { 131 return nil, errors.New("identity could not be determine automatically via Choria or was not supplied") 132 } 133 134 return p, p.reinit() 135 } 136 137 func (s *PuppetSecurity) reinit() error { 138 var err error 139 140 fc := filesec.Config{ 141 AllowList: s.conf.AllowList, 142 DisableTLSVerify: s.conf.DisableTLSVerify, 143 PrivilegedUsers: s.conf.PrivilegedUsers, 144 CA: s.caPath(), 145 Certificate: s.publicCertPath(), 146 Key: s.privateKeyPath(), 147 Identity: s.conf.Identity, 148 RemoteSignerURL: s.conf.RemoteSignerURL, 149 RemoteSignerTokenFile: s.conf.RemoteSignerTokenFile, 150 TLSConfig: s.conf.TLSConfig, 151 IdentitySuffix: s.conf.IdentitySuffix, 152 BackwardCompatVerification: true, 153 RemoteSigner: s.conf.RemoteSigner, 154 } 155 156 s.fsec, err = filesec.New(filesec.WithConfig(&fc), filesec.WithLog(s.log)) 157 if err != nil { 158 return err 159 } 160 161 if fc.BackwardCompatVerification { 162 s.log.Debugf("Puppet security system requesting legacy TLS support") 163 } else { 164 s.log.Debugf("Puppet security system supporting only new certificates") 165 } 166 167 return nil 168 } 169 170 func (s *PuppetSecurity) BackingTechnology() inter.SecurityTechnology { 171 return s.fsec.BackingTechnology() 172 } 173 174 // Provider reports the name of the security provider 175 func (s *PuppetSecurity) Provider() string { 176 return "puppet" 177 } 178 179 func (s *PuppetSecurity) TokenBytes() ([]byte, error) { 180 return nil, fmt.Errorf("tokens not available for puppet security provider") 181 } 182 183 // Enroll sends a CSR to the PuppetCA and wait for it to be signed 184 func (s *PuppetSecurity) Enroll(ctx context.Context, wait time.Duration, cb func(digest string, try int)) error { 185 if s.privateKeyExists() && s.caExists() && s.publicCertExists() { 186 return errors.New("already have all files needed for SSL operations") 187 } 188 189 err := s.createSSLDirectories() 190 if err != nil { 191 return fmt.Errorf("could not initialize ssl directories: %s", err) 192 } 193 194 var key *rsa.PrivateKey 195 196 if s.privateKeyExists() { 197 s.log.Debugf("Loading existing private key for %s", s.Identity()) 198 key, err = s.readPrivateKey() 199 if err != nil { 200 return fmt.Errorf("could not read private key for %s: %s", s.Identity(), err) 201 } 202 } else { 203 s.log.Debugf("Creating a new Private Key %s", s.Identity()) 204 205 key, err = s.writePrivateKey() 206 if err != nil { 207 return fmt.Errorf("could not write a new private key: %s", err) 208 } 209 } 210 211 if !s.caExists() { 212 s.log.Debug("Fetching CA") 213 214 err = s.fetchCA() 215 if err != nil { 216 return fmt.Errorf("could not fetch CA: %s", err) 217 } 218 } 219 220 previousCSR := s.csrExists() 221 var digest string 222 223 if !previousCSR { 224 s.log.Debugf("Creating a new CSR for %s", s.Identity()) 225 226 digest, err = s.writeCSR(key, s.Identity(), "choria.io") 227 if err != nil { 228 return fmt.Errorf("could not write CSR: %s", err) 229 } 230 } 231 232 if !s.publicCertExists() { 233 s.log.Debug("Submitting CSR to the PuppetCA") 234 235 err = s.submitCSR() 236 if err != nil { 237 if previousCSR { 238 s.log.Warnf("Submitting CSR failed, ignoring failure as this might be a continuation of a previous attempts: %s", err) 239 } else { 240 return fmt.Errorf("could not submit csr: %s", err) 241 } 242 } 243 } 244 245 timeout := time.NewTimer(wait).C 246 ticks := time.NewTicker(10 * time.Second).C 247 248 complete := make(chan int, 2) 249 250 attempt := 1 251 252 fetcher := func() { 253 cb(digest, attempt) 254 attempt++ 255 256 err := s.fetchCert() 257 if err != nil { 258 s.log.Debugf("Error while fetching cert on attempt %d: %s", attempt-1, err) 259 return 260 } 261 262 complete <- 1 263 } 264 265 fetcher() 266 267 for { 268 select { 269 case <-ctx.Done(): 270 return fmt.Errorf("interrupted") 271 case <-timeout: 272 return fmt.Errorf("timed out waiting for a certificate") 273 case <-complete: 274 return nil 275 case <-ticks: 276 fetcher() 277 } 278 } 279 } 280 281 // Validate determines if the node represents a valid SSL configuration 282 func (s *PuppetSecurity) Validate() ([]string, bool) { 283 errors := []string{} 284 285 ferrs, _ := s.fsec.Validate() 286 errors = append(errors, ferrs...) 287 288 return errors, len(errors) == 0 289 } 290 291 // RemoteSignRequest signs a choria request using a remote signer and returns a secure request 292 func (s *PuppetSecurity) RemoteSignRequest(ctx context.Context, str []byte) (signed []byte, err error) { 293 return s.fsec.RemoteSignRequest(ctx, str) 294 } 295 296 func (s *PuppetSecurity) IsRemoteSigning() bool { 297 return s.fsec.IsRemoteSigning() 298 } 299 300 // ChecksumBytes calculates a sha256 checksum for data 301 func (s *PuppetSecurity) ChecksumBytes(data []byte) []byte { 302 return s.fsec.ChecksumBytes(data) 303 } 304 305 // SignBytes signs a message using a SHA256 PKCS1v15 protocol 306 func (s *PuppetSecurity) SignBytes(str []byte) ([]byte, error) { 307 return s.fsec.SignBytes(str) 308 } 309 310 // VerifyByteSignature verify that dat matches signature sig made by the key, if pub cert is empty the active public key will be used 311 func (s *PuppetSecurity) VerifySignatureBytes(dat []byte, sig []byte, public ...[]byte) (should bool, signer string) { 312 return s.fsec.VerifySignatureBytes(dat, sig, public...) 313 } 314 315 // CallerName creates a choria like caller name in the form of choria=identity 316 func (s *PuppetSecurity) CallerName() string { 317 return s.fsec.CallerName() 318 } 319 320 // CallerIdentity extracts the identity from a choria like caller name in the form of choria=identity 321 func (s *PuppetSecurity) CallerIdentity(caller string) (string, error) { 322 return s.fsec.CallerIdentity(caller) 323 } 324 325 // ShouldAllowCaller verifies the public data 326 func (s *PuppetSecurity) ShouldAllowCaller(name string, callers ...[]byte) (privileged bool, err error) { 327 return s.fsec.ShouldAllowCaller(name, callers...) 328 } 329 330 // VerifyCertificate verifies a certificate is signed with the configured CA and if 331 // name is not "" that it matches the name given 332 func (s *PuppetSecurity) VerifyCertificate(certpem []byte, name string) error { 333 return s.fsec.VerifyCertificate(certpem, name) 334 } 335 336 // PublicCertBytes retrieves pem data in textual form for the public certificate of the current identity 337 func (s *PuppetSecurity) PublicCertBytes() ([]byte, error) { 338 return s.fsec.PublicCertBytes() 339 } 340 341 // PublicCert is the parsed public certificate 342 func (s *PuppetSecurity) PublicCert() (*x509.Certificate, error) { 343 return s.fsec.PublicCert() 344 } 345 346 // Identity determines the choria certname 347 func (s *PuppetSecurity) Identity() string { 348 return s.conf.Identity 349 } 350 351 // TLSConfig creates a TLS configuration for use by NATS, HTTPS etc 352 func (s *PuppetSecurity) TLSConfig() (*tls.Config, error) { 353 return s.fsec.TLSConfig() 354 } 355 356 // ClientTLSConfig creates a TLS configuration for use by NATS, HTTPS etc 357 func (s *PuppetSecurity) ClientTLSConfig() (*tls.Config, error) { 358 return s.fsec.ClientTLSConfig() 359 } 360 361 // SSLContext creates a SSL context loaded with our certs and ca 362 func (s *PuppetSecurity) SSLContext() (*http.Transport, error) { 363 return s.fsec.SSLContext() 364 } 365 366 func (s *PuppetSecurity) caPath() string { 367 return filepath.FromSlash(filepath.Join(s.sslDir(), "certs", "ca.pem")) 368 } 369 370 func (s *PuppetSecurity) privateKeyDir() string { 371 return filepath.FromSlash(filepath.Join(s.sslDir(), "private_keys")) 372 } 373 374 func (s *PuppetSecurity) privateKeyPath() string { 375 return filepath.FromSlash(filepath.Join(s.privateKeyDir(), fmt.Sprintf("%s.pem", s.Identity()))) 376 } 377 378 func (s *PuppetSecurity) createSSLDirectories() error { 379 ssl := s.sslDir() 380 381 err := os.MkdirAll(ssl, 0771) 382 if err != nil { 383 return err 384 } 385 386 for _, dir := range []string{"certificate_requests", "certs", "public_keys"} { 387 path := filepath.FromSlash(filepath.Join(ssl, dir)) 388 err = os.MkdirAll(path, 0755) 389 if err != nil { 390 return err 391 } 392 } 393 394 for _, dir := range []string{"private_keys", "private"} { 395 path := filepath.FromSlash(filepath.Join(ssl, dir)) 396 err = os.MkdirAll(path, 0750) 397 if err != nil { 398 return err 399 } 400 } 401 402 return nil 403 } 404 405 func (s *PuppetSecurity) csrPath() string { 406 return filepath.FromSlash((filepath.Join(s.sslDir(), "certificate_requests", fmt.Sprintf("%s.pem", s.Identity())))) 407 } 408 409 func (s *PuppetSecurity) publicCertPath() string { 410 return filepath.FromSlash((filepath.Join(s.sslDir(), "certs", fmt.Sprintf("%s.pem", s.Identity())))) 411 } 412 413 func (s *PuppetSecurity) sslDir() string { 414 return s.conf.SSLDir 415 } 416 417 func (s *PuppetSecurity) writeCSR(key *rsa.PrivateKey, cn string, ou string) (string, error) { 418 if s.csrExists() { 419 return "", fmt.Errorf("a certificate request already exist for %s", s.Identity()) 420 } 421 422 path := s.csrPath() 423 424 subj := pkix.Name{ 425 CommonName: cn, 426 OrganizationalUnit: []string{ou}, 427 } 428 429 asn1Subj, err := asn1.Marshal(subj.ToRDNSequence()) 430 if err != nil { 431 return "", fmt.Errorf("could not create subject: %s", err) 432 } 433 434 template := x509.CertificateRequest{ 435 RawSubject: asn1Subj, 436 SignatureAlgorithm: x509.SHA256WithRSA, 437 } 438 439 template.DNSNames = append(template.DNSNames, s.conf.AltNames...) 440 441 csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, key) 442 if err != nil { 443 return "", fmt.Errorf("could not create csr: %s", err) 444 } 445 446 csr, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0640) 447 if err != nil { 448 return "", fmt.Errorf("could not open csr %s for writing: %s", path, err) 449 } 450 defer csr.Close() 451 452 err = pem.Encode(csr, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) 453 if err != nil { 454 return "", fmt.Errorf("could not encode csr into %s: %s", path, err) 455 } 456 457 return fmt.Sprintf("%x", sha256.Sum256(csrBytes)), nil 458 } 459 460 func (s *PuppetSecurity) puppetCA() srvcache.Server { 461 found := srvcache.NewServer(s.conf.PuppetCAHost, s.conf.PuppetCAPort, "https") 462 463 if s.conf.DisableSRV || s.res == nil { 464 return found 465 } 466 467 servers, err := s.res.QuerySrvRecords([]string{"_x-puppet-ca._tcp", "_x-puppet._tcp"}) 468 if err != nil { 469 s.log.Warnf("Could not resolve Puppet CA SRV records: %s", err) 470 return found 471 } 472 473 if servers.Count() == 0 { 474 return found 475 } 476 477 found = servers.Servers()[0] 478 479 if found.Scheme() == "" { 480 found.SetScheme("https") 481 } 482 483 return found 484 } 485 486 func (s *PuppetSecurity) fetchCert() error { 487 if s.publicCertExists() { 488 return nil 489 } 490 491 server := s.puppetCA() 492 url := fmt.Sprintf("%s://%s:%d/puppet-ca/v1/certificate/%s?environment=production", server.Scheme(), server.Host(), server.Port(), s.Identity()) 493 494 req, err := http.NewRequest("GET", url, nil) 495 if err != nil { 496 return fmt.Errorf("could not create http request: %s", err) 497 } 498 499 req.Header.Set("Content-Type", "text/plain") 500 req.Header.Set("User-Agent", "Choria Orchestrator - http://choria.io") 501 502 client, err := s.HTTPClient(server.Scheme() == "https") 503 if err != nil { 504 return fmt.Errorf("could not set up HTTP connection: %s", err) 505 } 506 507 resp, err := client.Do(req) 508 if err != nil { 509 return fmt.Errorf("could not fetch certificate: %s", err) 510 } 511 defer resp.Body.Close() 512 513 if resp.StatusCode != 200 { 514 return fmt.Errorf("could not fetch certificate: %d", resp.StatusCode) 515 } 516 517 body, err := io.ReadAll(resp.Body) 518 if err != nil { 519 return fmt.Errorf("could not read response body: %s", err) 520 } 521 522 err = os.WriteFile(s.publicCertPath(), body, 0644) 523 if err != nil { 524 return err 525 } 526 527 return nil 528 } 529 530 func (s *PuppetSecurity) fetchCA() error { 531 if s.caExists() { 532 return nil 533 } 534 535 server := s.puppetCA() 536 url := fmt.Sprintf("%s://%s:%d/puppet-ca/v1/certificate/ca?environment=production", server.Scheme(), server.Host(), server.Port()) 537 538 // specifically disabling verification as at this point we do not have 539 // the CA needed to do verification, there's no choice in the matter 540 // really and this is just how its designed to work 541 tr := &http.Transport{ 542 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 543 } 544 545 client := &http.Client{Transport: tr} 546 547 resp, err := client.Get(url) 548 if err != nil { 549 return err 550 } 551 defer resp.Body.Close() 552 553 body, err := io.ReadAll(resp.Body) 554 if err != nil { 555 return fmt.Errorf("could not read response body: %s", err) 556 } 557 558 if resp.StatusCode != 200 { 559 return errors.New(string(body)) 560 } 561 562 err = os.WriteFile(s.caPath(), body, 0644) 563 if err != nil { 564 return err 565 } 566 567 return nil 568 } 569 570 func (s *PuppetSecurity) submitCSR() error { 571 csr, err := s.csrTXT() 572 if err != nil { 573 return fmt.Errorf("could not read CSR: %s", err) 574 } 575 576 server := s.puppetCA() 577 578 url := fmt.Sprintf("%s://%s:%d/puppet-ca/v1/certificate_request/%s?environment=production", server.Scheme(), server.Host(), server.Port(), s.Identity()) 579 580 req, err := http.NewRequest("PUT", url, bytes.NewBuffer(csr)) 581 if err != nil { 582 return fmt.Errorf("could not create http request: %s", err) 583 } 584 585 req.Header.Set("Content-Type", "text/plain") 586 req.Header.Set("User-Agent", "Choria Orchestrator - http://choria.io") 587 588 req.Host = server.Host() 589 590 client, err := s.HTTPClient(server.Scheme() == "https") 591 if err != nil { 592 return fmt.Errorf("could not set up HTTP connection: %s", err) 593 } 594 595 resp, err := client.Do(req) 596 if err != nil { 597 return fmt.Errorf("could not send CSR: %s", err) 598 } 599 defer resp.Body.Close() 600 601 if resp.StatusCode == 200 { 602 return nil 603 } 604 605 body, err := io.ReadAll(resp.Body) 606 if err != nil { 607 return fmt.Errorf("could not read response body: %s", err) 608 } 609 610 if len(body) > 0 { 611 return fmt.Errorf("could not send CSR to %s://%s:%d: %s: %s", server.Scheme(), server.Host(), server.Port(), resp.Status, string(body)) 612 } 613 614 return fmt.Errorf("could not send CSR to %s://%s:%d: %s", server.Scheme(), server.Host(), server.Port(), resp.Status) 615 } 616 617 // HTTPClient creates a standard HTTP client with optional security, it will 618 // be set to use the CA and client certs for auth. servername should match the 619 // remote hosts name for SNI 620 func (s *PuppetSecurity) HTTPClient(secure bool) (*http.Client, error) { 621 return s.fsec.HTTPClient(secure) 622 } 623 624 func (s *PuppetSecurity) csrTXT() ([]byte, error) { 625 return os.ReadFile(s.csrPath()) 626 } 627 628 func (s *PuppetSecurity) readPrivateKey() (*rsa.PrivateKey, error) { 629 if !s.privateKeyExists() { 630 return nil, fmt.Errorf("key not found in %s", s.privateKeyPath()) 631 } 632 633 pd, err := os.ReadFile(s.privateKeyPath()) 634 if err != nil { 635 return nil, err 636 } 637 638 privPem, _ := pem.Decode(pd) 639 if privPem.Type != "RSA PRIVATE KEY" { 640 return nil, fmt.Errorf("key file %s did not contain a private key", s.privateKeyPath()) 641 } 642 643 parsedKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) 644 if err != nil { 645 return nil, err 646 } 647 648 return parsedKey, nil 649 } 650 651 func (s *PuppetSecurity) writePrivateKey() (*rsa.PrivateKey, error) { 652 if s.privateKeyExists() { 653 return nil, fmt.Errorf("a private key already exist for %s", s.Identity()) 654 } 655 656 key, err := rsa.GenerateKey(rand.Reader, 2048) 657 if err != nil { 658 return nil, fmt.Errorf("could not generate rsa key: %s", err) 659 } 660 661 pemdata := pem.EncodeToMemory( 662 &pem.Block{ 663 Type: "RSA PRIVATE KEY", 664 Bytes: x509.MarshalPKCS1PrivateKey(key), 665 }, 666 ) 667 668 err = os.WriteFile(s.privateKeyPath(), pemdata, 0640) 669 if err != nil { 670 return nil, fmt.Errorf("could not write private key: %s", err) 671 } 672 673 return key, nil 674 } 675 676 func (s *PuppetSecurity) csrExists() bool { 677 return util.FileExist(s.csrPath()) 678 } 679 680 func (s *PuppetSecurity) privateKeyExists() bool { 681 return util.FileExist(s.privateKeyPath()) 682 } 683 684 func (s *PuppetSecurity) publicCertExists() bool { 685 return util.FileExist(s.publicCertPath()) 686 } 687 688 func (s *PuppetSecurity) caExists() bool { 689 return util.FileExist(s.caPath()) 690 } 691 692 func (s *PuppetSecurity) ShouldSignReplies() bool { return false }