github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/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/models"
    11  	"github.com/StackExchange/dnscontrol/pkg/acme"
    12  	"github.com/StackExchange/dnscontrol/pkg/normalize"
    13  	"github.com/urfave/cli"
    14  )
    15  
    16  var _ = cmd(catUtils, func() *cli.Command {
    17  	var args GetCertsArgs
    18  	return &cli.Command{
    19  		Name:  "get-certs",
    20  		Usage: "Issue certificates via Let's Encrypt",
    21  		Action: func(c *cli.Context) error {
    22  			return exit(GetCerts(args))
    23  		},
    24  		Flags: args.flags(),
    25  	}
    26  }())
    27  
    28  type GetCertsArgs struct {
    29  	GetDNSConfigArgs
    30  	GetCredentialsArgs
    31  
    32  	ACMEServer     string
    33  	CertsFile      string
    34  	RenewUnderDays int
    35  	CertDirectory  string
    36  	Email          string
    37  	AgreeTOS       bool
    38  	Verbose        bool
    39  
    40  	IgnoredProviders string
    41  }
    42  
    43  func (args *GetCertsArgs) flags() []cli.Flag {
    44  	flags := args.GetDNSConfigArgs.flags()
    45  	flags = append(flags, args.GetCredentialsArgs.flags()...)
    46  
    47  	flags = append(flags, cli.StringFlag{
    48  		Name:        "acme",
    49  		Destination: &args.ACMEServer,
    50  		Value:       "live",
    51  		Usage:       `ACME server to issue against. Give full directory endpoint. Can also use 'staging' or 'live' for standard Let's Encrpyt endpoints.`,
    52  	})
    53  	flags = append(flags, cli.IntFlag{
    54  		Name:        "renew",
    55  		Destination: &args.RenewUnderDays,
    56  		Value:       15,
    57  		Usage:       `Renew certs with less than this many days remaining`,
    58  	})
    59  	flags = append(flags, cli.StringFlag{
    60  		Name:        "dir",
    61  		Destination: &args.CertDirectory,
    62  		Value:       ".",
    63  		Usage:       `Directory to store certificates and other data`,
    64  	})
    65  	flags = append(flags, cli.StringFlag{
    66  		Name:        "certConfig",
    67  		Destination: &args.CertsFile,
    68  		Value:       "certs.json",
    69  		Usage:       `Json file containing list of certificates to issue`,
    70  	})
    71  	flags = append(flags, cli.StringFlag{
    72  		Name:        "email",
    73  		Destination: &args.Email,
    74  		Value:       "",
    75  		Usage:       `Email to register with let's encrypt`,
    76  	})
    77  	flags = append(flags, cli.BoolFlag{
    78  		Name:        "agreeTOS",
    79  		Destination: &args.AgreeTOS,
    80  		Usage:       `Must provide this to agree to Let's Encrypt terms of service`,
    81  	})
    82  	flags = append(flags, cli.StringFlag{
    83  		Name:        "skip",
    84  		Destination: &args.IgnoredProviders,
    85  		Value:       "",
    86  		Usage:       `Provider names to not use for challenges (comma separated)`,
    87  	})
    88  	flags = append(flags, cli.BoolFlag{
    89  		Name:        "verbose",
    90  		Destination: &args.Verbose,
    91  		Usage:       "Enable detailed logging from acme library",
    92  	})
    93  	return flags
    94  }
    95  
    96  func GetCerts(args GetCertsArgs) error {
    97  	fmt.Println(args.JSFile)
    98  	// check agree flag
    99  	if !args.AgreeTOS {
   100  		return fmt.Errorf("You must agree to the Let's Encrypt Terms of Service by using -agreeTOS")
   101  	}
   102  	if args.Email == "" {
   103  		return fmt.Errorf("Must provide email to use for Let's Encrypt registration")
   104  	}
   105  
   106  	// load dns config
   107  	cfg, err := GetDNSConfig(args.GetDNSConfigArgs)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	errs := normalize.NormalizeAndValidateConfig(cfg)
   112  	if PrintValidationErrors(errs) {
   113  		return fmt.Errorf("Exiting due to validation errors")
   114  	}
   115  	_, err = InitializeProviders(args.CredsFile, cfg, false)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	for _, skip := range strings.Split(args.IgnoredProviders, ",") {
   121  		acme.IgnoredProviders[skip] = true
   122  	}
   123  
   124  	// load cert list
   125  	certList := []*acme.CertConfig{}
   126  	f, err := os.Open(args.CertsFile)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	defer f.Close()
   131  	dec := json.NewDecoder(f)
   132  	err = dec.Decode(&certList)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	if len(certList) == 0 {
   137  		return fmt.Errorf("Must provide at least one certificate to issue in cert configuration")
   138  	}
   139  	if err = validateCertificateList(certList, cfg); err != nil {
   140  		return err
   141  	}
   142  	acmeServer := args.ACMEServer
   143  	if acmeServer == "live" {
   144  		acmeServer = acme.LetsEncryptLive
   145  	} else if acmeServer == "staging" {
   146  		acmeServer = acme.LetsEncryptStage
   147  	}
   148  	client, err := acme.New(cfg, args.CertDirectory, args.Email, acmeServer)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	for _, cert := range certList {
   153  		_, err := client.IssueOrRenewCert(cert, args.RenewUnderDays, args.Verbose)
   154  		if err != nil {
   155  			return err
   156  		}
   157  	}
   158  	return nil
   159  }
   160  
   161  var validCertNamesRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_\-]*$`)
   162  
   163  func validateCertificateList(certs []*acme.CertConfig, cfg *models.DNSConfig) error {
   164  	for _, cert := range certs {
   165  		name := cert.CertName
   166  		if !validCertNamesRegex.MatchString(name) {
   167  			return fmt.Errorf("'%s' is not a valud certificate name. Only alphanumerics, - and _ allowed", name)
   168  		}
   169  		sans := cert.Names
   170  		if len(sans) > 100 {
   171  			return fmt.Errorf("certificate '%s' has too many SANs. Max of 100", name)
   172  		}
   173  		if len(sans) == 0 {
   174  			return fmt.Errorf("certificate '%s' needs at least one SAN", name)
   175  		}
   176  		for _, san := range sans {
   177  			d := cfg.DomainContainingFQDN(san)
   178  			if d == nil {
   179  				return fmt.Errorf("DNS config has no domain that matches SAN '%s'", san)
   180  			}
   181  		}
   182  	}
   183  	return nil
   184  }