github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/commands/getCerts.go (about)

     1  package commands
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/StackExchange/dnscontrol/v2/models"
    11  	"github.com/StackExchange/dnscontrol/v2/pkg/acme"
    12  	"github.com/StackExchange/dnscontrol/v2/pkg/normalize"
    13  	"github.com/StackExchange/dnscontrol/v2/pkg/printer"
    14  	"github.com/urfave/cli/v2"
    15  )
    16  
    17  var _ = cmd(catUtils, func() *cli.Command {
    18  	var args GetCertsArgs
    19  	return &cli.Command{
    20  		Name:  "get-certs",
    21  		Usage: "Issue certificates via Let's Encrypt",
    22  		Action: func(c *cli.Context) error {
    23  			return exit(GetCerts(args))
    24  		},
    25  		Flags: args.flags(),
    26  	}
    27  }())
    28  
    29  // GetCertsArgs stores the flags and arguments common to cert commands
    30  type GetCertsArgs struct {
    31  	GetDNSConfigArgs
    32  	GetCredentialsArgs
    33  
    34  	ACMEServer     string
    35  	CertsFile      string
    36  	RenewUnderDays int
    37  	CertDirectory  string
    38  	Email          string
    39  	AgreeTOS       bool
    40  	Verbose        bool
    41  	Vault          bool
    42  	VaultPath      string
    43  	Only           string
    44  
    45  	Notify bool
    46  
    47  	IgnoredProviders string
    48  }
    49  
    50  func (args *GetCertsArgs) flags() []cli.Flag {
    51  	flags := args.GetDNSConfigArgs.flags()
    52  	flags = append(flags, args.GetCredentialsArgs.flags()...)
    53  
    54  	flags = append(flags, &cli.StringFlag{
    55  		Name:        "acme",
    56  		Destination: &args.ACMEServer,
    57  		Value:       "live",
    58  		Usage:       `ACME server to issue against. Give full directory endpoint. Can also use 'staging' or 'live' for standard Let's Encrpyt endpoints.`,
    59  	})
    60  	flags = append(flags, &cli.IntFlag{
    61  		Name:        "renew",
    62  		Destination: &args.RenewUnderDays,
    63  		Value:       15,
    64  		Usage:       `Renew certs with less than this many days remaining`,
    65  	})
    66  	flags = append(flags, &cli.StringFlag{
    67  		Name:        "dir",
    68  		Destination: &args.CertDirectory,
    69  		Value:       ".",
    70  		Usage:       `Directory to store certificates and other data`,
    71  	})
    72  	flags = append(flags, &cli.StringFlag{
    73  		Name:        "certConfig",
    74  		Destination: &args.CertsFile,
    75  		Value:       "certs.json",
    76  		Usage:       `Json file containing list of certificates to issue`,
    77  	})
    78  	flags = append(flags, &cli.StringFlag{
    79  		Name:        "email",
    80  		Destination: &args.Email,
    81  		Value:       "",
    82  		Usage:       `Email to register with let's encrypt`,
    83  	})
    84  	flags = append(flags, &cli.BoolFlag{
    85  		Name:        "agreeTOS",
    86  		Destination: &args.AgreeTOS,
    87  		Usage:       `Must provide this to agree to Let's Encrypt terms of service`,
    88  	})
    89  	flags = append(flags, &cli.BoolFlag{
    90  		Name:        "vault",
    91  		Destination: &args.Vault,
    92  		Usage:       `Store certificates as secrets in hashicorp vault instead of on disk.`,
    93  	})
    94  	flags = append(flags, &cli.StringFlag{
    95  		Name:        "vaultPath",
    96  		Destination: &args.VaultPath,
    97  		Value:       "/secret/certs",
    98  		Usage:       `Path in vault to store certificates`,
    99  	})
   100  	flags = append(flags, &cli.StringFlag{
   101  		Name:        "skip",
   102  		Destination: &args.IgnoredProviders,
   103  		Value:       "",
   104  		Usage:       `Provider names to not use for challenges (comma separated)`,
   105  	})
   106  	flags = append(flags, &cli.BoolFlag{
   107  		Name:        "verbose",
   108  		Destination: &args.Verbose,
   109  		Usage:       "Enable detailed logging (deprecated: use the global -v flag)",
   110  	})
   111  	flags = append(flags, &cli.BoolFlag{
   112  		Name:        "notify",
   113  		Destination: &args.Notify,
   114  		Usage:       `set to true to send notifications to configured destinations`,
   115  	})
   116  	flags = append(flags, &cli.StringFlag{
   117  		Name:        "only",
   118  		Destination: &args.Only,
   119  		Usage:       `Only check a single cert. Provide cert name.`,
   120  	})
   121  	return flags
   122  }
   123  
   124  // GetCerts implements the get-certs command.
   125  func GetCerts(args GetCertsArgs) error {
   126  	fmt.Println(args.JSFile)
   127  	// check agree flag
   128  	if !args.AgreeTOS {
   129  		return fmt.Errorf("You must agree to the Let's Encrypt Terms of Service by using -agreeTOS")
   130  	}
   131  	if args.Email == "" {
   132  		return fmt.Errorf("Must provide email to use for Let's Encrypt registration")
   133  	}
   134  
   135  	// load dns config
   136  	cfg, err := GetDNSConfig(args.GetDNSConfigArgs)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	errs := normalize.NormalizeAndValidateConfig(cfg)
   141  	if PrintValidationErrors(errs) {
   142  		return fmt.Errorf("Exiting due to validation errors")
   143  	}
   144  	notifier, err := InitializeProviders(args.CredsFile, cfg, args.Notify)
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	for _, skip := range strings.Split(args.IgnoredProviders, ",") {
   150  		acme.IgnoredProviders[skip] = true
   151  	}
   152  
   153  	// load cert list
   154  	certList := []*acme.CertConfig{}
   155  	f, err := os.Open(args.CertsFile)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	defer f.Close()
   160  	dec := json.NewDecoder(f)
   161  	err = dec.Decode(&certList)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	if len(certList) == 0 {
   166  		return fmt.Errorf("Must provide at least one certificate to issue in cert configuration")
   167  	}
   168  	if err = validateCertificateList(certList, cfg); err != nil {
   169  		return err
   170  	}
   171  
   172  	acmeServer := args.ACMEServer
   173  	if acmeServer == "live" {
   174  		acmeServer = acme.LetsEncryptLive
   175  	} else if acmeServer == "staging" {
   176  		acmeServer = acme.LetsEncryptStage
   177  	}
   178  
   179  	var client acme.Client
   180  
   181  	if args.Vault {
   182  		client, err = acme.NewVault(cfg, args.VaultPath, args.Email, acmeServer, notifier)
   183  	} else {
   184  		client, err = acme.New(cfg, args.CertDirectory, args.Email, acmeServer, notifier)
   185  	}
   186  	if err != nil {
   187  		return err
   188  	}
   189  	var manyerr error
   190  	for _, cert := range certList {
   191  		if args.Only != "" && cert.CertName != args.Only {
   192  			continue
   193  		}
   194  		v := args.Verbose || printer.DefaultPrinter.Verbose
   195  		issued, err := client.IssueOrRenewCert(cert, args.RenewUnderDays, v)
   196  		if issued || err != nil {
   197  			notifier.Notify(cert.CertName, "certificate", "Issued new certificate", err, false)
   198  		}
   199  		if err != nil {
   200  			if manyerr == nil {
   201  				manyerr = err
   202  			} else {
   203  				manyerr = fmt.Errorf("%w; %v", manyerr, err)
   204  			}
   205  		}
   206  	}
   207  	notifier.Done()
   208  	return manyerr
   209  }
   210  
   211  var validCertNamesRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_\-]*$`)
   212  
   213  func validateCertificateList(certs []*acme.CertConfig, cfg *models.DNSConfig) error {
   214  	for _, cert := range certs {
   215  		name := cert.CertName
   216  		if !validCertNamesRegex.MatchString(name) {
   217  			return fmt.Errorf("'%s' is not a valid certificate name. Only alphanumerics, - and _ allowed", name)
   218  		}
   219  		sans := cert.Names
   220  		if len(sans) > 100 {
   221  			return fmt.Errorf("certificate '%s' has too many SANs. Max of 100", name)
   222  		}
   223  		if len(sans) == 0 {
   224  			return fmt.Errorf("certificate '%s' needs at least one SAN", name)
   225  		}
   226  		for _, san := range sans {
   227  			d := cfg.DomainContainingFQDN(san)
   228  			if d == nil {
   229  				return fmt.Errorf("DNS config has no domain that matches SAN '%s'", san)
   230  			}
   231  		}
   232  	}
   233  	return nil
   234  }