mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-09-07 21:54:59 +08:00
parent
50e4bfb4e0
commit
188343d822
1 changed files with 242 additions and 0 deletions
242
commands/getCerts.go
Normal file
242
commands/getCerts.go
Normal file
|
@ -0,0 +1,242 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/acme"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/credsfile"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/normalize"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var _ = cmd(catUtils, func() *cli.Command {
|
||||
var args GetCertsArgs
|
||||
return &cli.Command{
|
||||
Name: "get-certs",
|
||||
Usage: "DEPRECATED: Issue certificates via Let's Encrypt",
|
||||
Action: func(c *cli.Context) error {
|
||||
return exit(GetCerts(args))
|
||||
},
|
||||
Flags: args.flags(),
|
||||
|
||||
Hidden: true,
|
||||
}
|
||||
}())
|
||||
|
||||
// GetCertsArgs stores the flags and arguments common to cert commands
|
||||
type GetCertsArgs struct {
|
||||
GetDNSConfigArgs
|
||||
GetCredentialsArgs
|
||||
|
||||
ACMEServer string
|
||||
CertsFile string
|
||||
RenewUnderDays int
|
||||
CertDirectory string
|
||||
Email string
|
||||
AgreeTOS bool
|
||||
Verbose bool
|
||||
Vault bool
|
||||
VaultPath string
|
||||
Only string
|
||||
|
||||
Notify bool
|
||||
|
||||
IgnoredProviders string
|
||||
}
|
||||
|
||||
func (args *GetCertsArgs) flags() []cli.Flag {
|
||||
flags := args.GetDNSConfigArgs.flags()
|
||||
flags = append(flags, args.GetCredentialsArgs.flags()...)
|
||||
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "acme",
|
||||
Destination: &args.ACMEServer,
|
||||
Value: "live",
|
||||
Usage: `ACME server to issue against. Give full directory endpoint. Can also use 'staging' or 'live' for standard Let's Encrypt endpoints.`,
|
||||
})
|
||||
flags = append(flags, &cli.IntFlag{
|
||||
Name: "renew",
|
||||
Destination: &args.RenewUnderDays,
|
||||
Value: 15,
|
||||
Usage: `Renew certs with less than this many days remaining`,
|
||||
})
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "dir",
|
||||
Destination: &args.CertDirectory,
|
||||
Value: ".",
|
||||
Usage: `Directory to store certificates and other data`,
|
||||
})
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "certConfig",
|
||||
Destination: &args.CertsFile,
|
||||
Value: "certs.json",
|
||||
Usage: `Json file containing list of certificates to issue`,
|
||||
})
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "email",
|
||||
Destination: &args.Email,
|
||||
Value: "",
|
||||
Usage: `Email to register with let's encrypt`,
|
||||
})
|
||||
flags = append(flags, &cli.BoolFlag{
|
||||
Name: "agreeTOS",
|
||||
Destination: &args.AgreeTOS,
|
||||
Usage: `Must provide this to agree to Let's Encrypt terms of service`,
|
||||
})
|
||||
flags = append(flags, &cli.BoolFlag{
|
||||
Name: "vault",
|
||||
Destination: &args.Vault,
|
||||
Usage: `Store certificates as secrets in hashicorp vault instead of on disk.`,
|
||||
})
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "vaultPath",
|
||||
Destination: &args.VaultPath,
|
||||
Value: "/secret/certs",
|
||||
Usage: `Path in vault to store certificates`,
|
||||
})
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "skip",
|
||||
Destination: &args.IgnoredProviders,
|
||||
Value: "",
|
||||
Usage: `Provider names to not use for challenges (comma separated)`,
|
||||
})
|
||||
flags = append(flags, &cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Destination: &args.Verbose,
|
||||
Usage: "Enable detailed logging (deprecated: use the global -v flag)",
|
||||
})
|
||||
flags = append(flags, &cli.BoolFlag{
|
||||
Name: "notify",
|
||||
Destination: &args.Notify,
|
||||
Usage: `set to true to send notifications to configured destinations`,
|
||||
})
|
||||
flags = append(flags, &cli.StringFlag{
|
||||
Name: "only",
|
||||
Destination: &args.Only,
|
||||
Usage: `Only check a single cert. Provide cert name.`,
|
||||
})
|
||||
return flags
|
||||
}
|
||||
|
||||
// GetCerts implements the get-certs command.
|
||||
func GetCerts(args GetCertsArgs) error {
|
||||
fmt.Println(args.JSFile)
|
||||
// check agree flag
|
||||
if !args.AgreeTOS {
|
||||
return errors.New("you must agree to the Let's Encrypt Terms of Service by using -agreeTOS")
|
||||
}
|
||||
if args.Email == "" {
|
||||
return errors.New("must provide email to use for Let's Encrypt registration")
|
||||
}
|
||||
|
||||
// load dns config
|
||||
cfg, err := GetDNSConfig(args.GetDNSConfigArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
errs := normalize.ValidateAndNormalizeConfig(cfg)
|
||||
if PrintValidationErrors(errs) {
|
||||
return errors.New("exiting due to validation errors")
|
||||
}
|
||||
providerConfigs, err := credsfile.LoadProviderConfigs(args.CredsFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notifier, err := InitializeProviders(cfg, providerConfigs, args.Notify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, skip := range strings.Split(args.IgnoredProviders, ",") {
|
||||
acme.IgnoredProviders[skip] = true
|
||||
}
|
||||
|
||||
// load cert list
|
||||
certList := []*acme.CertConfig{}
|
||||
f, err := os.Open(args.CertsFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
dec := json.NewDecoder(f)
|
||||
err = dec.Decode(&certList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(certList) == 0 {
|
||||
return errors.New("must provide at least one certificate to issue in cert configuration")
|
||||
}
|
||||
if err = validateCertificateList(certList, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
acmeServer := args.ACMEServer
|
||||
if acmeServer == "live" {
|
||||
acmeServer = acme.LetsEncryptLive
|
||||
} else if acmeServer == "staging" {
|
||||
acmeServer = acme.LetsEncryptStage
|
||||
}
|
||||
|
||||
var client acme.Client
|
||||
|
||||
if args.Vault {
|
||||
client, err = acme.NewVault(cfg, args.VaultPath, args.Email, acmeServer, notifier)
|
||||
} else {
|
||||
client, err = acme.New(cfg, args.CertDirectory, args.Email, acmeServer, notifier)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var manyerr error
|
||||
for _, cert := range certList {
|
||||
if args.Only != "" && cert.CertName != args.Only {
|
||||
continue
|
||||
}
|
||||
v := args.Verbose || printer.DefaultPrinter.Verbose
|
||||
issued, err := client.IssueOrRenewCert(cert, args.RenewUnderDays, v)
|
||||
if issued || err != nil {
|
||||
notifier.Notify(cert.CertName, "certificate", "Issued new certificate", err, false)
|
||||
}
|
||||
if err != nil {
|
||||
if manyerr == nil {
|
||||
manyerr = err
|
||||
} else {
|
||||
manyerr = fmt.Errorf("%w; %w", manyerr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
notifier.Done()
|
||||
return manyerr
|
||||
}
|
||||
|
||||
var validCertNamesRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_\-]*$`)
|
||||
|
||||
func validateCertificateList(certs []*acme.CertConfig, cfg *models.DNSConfig) error {
|
||||
for _, cert := range certs {
|
||||
name := cert.CertName
|
||||
if !validCertNamesRegex.MatchString(name) {
|
||||
return fmt.Errorf("'%s' is not a valid certificate name. Only alphanumerics, - and _ allowed", name)
|
||||
}
|
||||
sans := cert.Names
|
||||
if len(sans) > 100 {
|
||||
return fmt.Errorf("certificate '%s' has too many SANs. Max of 100", name)
|
||||
}
|
||||
if len(sans) == 0 {
|
||||
return fmt.Errorf("certificate '%s' needs at least one SAN", name)
|
||||
}
|
||||
for _, san := range sans {
|
||||
d := cfg.DomainContainingFQDN(san)
|
||||
if d == nil {
|
||||
return fmt.Errorf("DNS config has no domain that matches SAN '%s'", san)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Add table
Reference in a new issue