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 }