diff --git a/.env.dev b/.env.dev index 49ce269..7fefd59 100644 --- a/.env.dev +++ b/.env.dev @@ -6,6 +6,7 @@ ACME_SERVER_URL=https://pebble:14000/dir DNS_ADDRESS=challtestsrv:8053 CERTIFICATOR_DOMAINS=mydomain.com,example.com CERTIFICATOR_RENEW_BEFORE_DAYS=30 +CERTIFICATOR_DOMAINS_LIST=mydomain.com,example.com CERTIFICATOR_DOMAINS_FILE=/app/fixtures/domains.yml ENVIRONMENT=dev EXEC_PATH=./fixtures/update-dns.sh diff --git a/README.md b/README.md index 9f7fad6..64ebf88 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ Configuration variables: - `LOG_LEVEL` - logging level, supported levels - DEBUG, INFO, WARN, ERROR, FATAL. Default: INFO. - `DNS_ADDRESS` - DNS server address that is used to check challenge DNS record propagation. Default: 127.0.0.1:53 - `ENVIRONMENT` - sets an environment where the certificator is running. If the environment is dev it uses token set in `VAULT_DEV_ROOT_TOKEN_ID` env variable to authenticate in Vault. If the environment is prod it uses an approle authentication method. Default: prod -- `CERTIFICATOR_DOMAINS_FILE` - path to a file where domains are defined. Default: /code/domains.yml +- `CERTIFICATOR_DOMAINS_FILE` - path to a file where domains are defined. Overridden by `CERTIFICATOR_DOMAINS_LIST`. Default: /code/domains.yml +- `CERTIFICATOR_DOMAINS_LIST` - allows specifying domains directly via an environment variable. If set(non-empty), this takes precedence over loading domains from the DomainsFile (`CERTIFICATOR_DOMAINS_FILE`). - `CERTIFICATOR_RENEW_BEFORE_DAYS` - set how many validity days should certificate have remaining before renewal. Default: 30 #### CNAME @@ -45,9 +46,31 @@ CA will verify domain ownership following the same scheme This allows giving this tool a token with access rights limited to a single DNS zone. -#### Domains file +#### Domains -Domains that the certificator should retrieve certificates for should be defined in this file in YAML format. An example file is in [domains.yml](domains.yml). +The application supports two ways to configure the list of domains that it should retrieve certificates for: + +### 1. Environment Variable: `CERTIFICATOR_DOMAINS_LIST` + +You can specify the list of domains directly via the `CERTIFICATOR_DOMAINS_LIST` environment variable. This is useful for containerised deployments or environments where editing files is inconvenient. The value should be a comma-separated list of domains. + +**Example:** + +```sh +export CERTIFICATOR_DOMAINS_LIST=example.com,example.org,sub.example.net +``` + +Note: **If this variable is set (non-empty), it takes precedence over file-based configuration.** + +### 2. Domains File: `CERTIFICATOR_DOMAINS_FILE` + +If `CERTIFICATOR_DOMAINS_LIST` is not set, the application will load domains from a YAML file specified by the `CERTIFICATOR_DOMAINS_FILE` environment variable. An example file is in [domains.yml](domains.yml), which is deployed to `/code/domains.yml` in the container and is the default value for this variable. + +**Example:** + +```sh +export CERTIFICATOR_DOMAINS_FILE=/path/to/my_domains.yml +``` Every item in the array under the `domains` key results in a certificate. The first domain in an array item is used for the CommonName field of the certificate, all other domains are added using the Subject Alternate Names extension. Domains in a single array item are separated by commas. The first domain is also used as a key in the Vault KV store. diff --git a/pkg/config/config.go b/pkg/config/config.go index 2a28595..17720fa 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -38,8 +38,9 @@ type Config struct { DNSAddress string `envconfig:"DNS_ADDRESS" default:"127.0.0.1:53"` Environment string `envconfig:"ENVIRONMENT" default:"prod"` DomainsFile string `envconfig:"CERTIFICATOR_DOMAINS_FILE" default:"/code/domains.yml"` + DomainsList []string `envconfig:"CERTIFICATOR_DOMAINS_LIST"` RenewBeforeDays int `envconfig:"CERTIFICATOR_RENEW_BEFORE_DAYS" default:"30"` - Domains []string `yaml:"domains"` + Domains []string } // LoadConfig loads configuration options to variable @@ -50,19 +51,42 @@ func LoadConfig() (Config, error) { return Config{}, errors.Wrapf(err, "failed getting config from env") } - f, err := os.Open(cfg.DomainsFile) + if len(cfg.DomainsList) > 0 { + cfg.Domains = cfg.DomainsList + + return cfg, err + } else { + cfg.Domains, err = parseDomainsFile(cfg.DomainsFile) + if err != nil { + return Config{}, err + } + + return cfg, err + } +} + +func parseDomainsFile(domainsFile string) ([]string, error) { + f, err := os.Open(domainsFile) if err != nil { - return Config{}, errors.Wrapf(err, "opening %s", cfg.DomainsFile) + return nil, errors.Wrapf(err, "opening %s", domainsFile) } content, err := ioutil.ReadAll(f) if err != nil { - return Config{}, errors.Wrapf(err, "reading content of %s", cfg.DomainsFile) + return nil, errors.Wrapf(err, "reading content of %s", domainsFile) } - if err := yaml.Unmarshal(content, &cfg); err != nil { - return Config{}, errors.Wrapf(err, "parsing %s", cfg.DomainsFile) + var contentMap map[string]interface{} + + if err := yaml.Unmarshal(content, &contentMap); err != nil { + return nil, errors.Wrapf(err, "parsing %s", domainsFile) + } + + var domains []string + + for _, v := range contentMap["domains"].([]interface{}) { + domains = append(domains, v.(string)) } - return cfg, err + return domains, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f8caa0a..d36432f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -32,6 +32,7 @@ func TestDefaultConfig(t *testing.T) { Environment: "prod", DomainsFile: "../../domains.yml", Domains: []string{"mydomain.com,www.mydomain.com", "example.com"}, + DomainsList: nil, RenewBeforeDays: 30, } @@ -40,7 +41,7 @@ func TestDefaultConfig(t *testing.T) { testutil.Equals(t, expectedConf, conf) } -func TestConfig(t *testing.T) { +func TestConfig_WithDomainsFile(t *testing.T) { var ( reregisterAcc bool = true acmeServerURL string = "http://someserver" @@ -76,6 +77,7 @@ func TestConfig(t *testing.T) { Environment: environment, DomainsFile: "../../domains.yml", Domains: []string{"mydomain.com,www.mydomain.com", "example.com"}, + DomainsList: nil, RenewBeforeDays: renewBeforeDays, } ) @@ -100,6 +102,68 @@ func TestConfig(t *testing.T) { testutil.Equals(t, expectedConf, conf) } +func TestConfig_WithDomainsList(t *testing.T) { + var ( + reregisterAcc bool = true + acmeServerURL string = "http://someserver" + dnsChallengeProvider string = "other" + dnsPropagationReq bool = false + vaultRoleID string = "role" + vaultSecretID string = "secret" + vaultKVStorePath string = "secret/path" + logFormat string = "LOGFMT" + logLevel string = "DEBUG" + dnsAddress string = "1.1.1.1:53" + environment string = "test" + renewBeforeDays int = 60 + + expectedConf = Config{ + Acme: Acme{ + AccountEmail: "test@test.com", + DNSChallengeProvider: dnsChallengeProvider, + DNSPropagationRequirement: dnsPropagationReq, + ReregisterAccount: reregisterAcc, + ServerURL: acmeServerURL, + }, + Vault: Vault{ + ApproleRoleID: vaultRoleID, + ApproleSecretID: vaultSecretID, + KVStoragePath: vaultKVStorePath, + }, + Log: Log{ + Format: logFormat, + Level: logLevel, + }, + DNSAddress: dnsAddress, + Environment: environment, + DomainsFile: "../../domains.yml", + DomainsList: []string{"mydomain.com", "www.mydomain.com", "example.com"}, + Domains: []string{"mydomain.com", "www.mydomain.com", "example.com"}, + RenewBeforeDays: renewBeforeDays, + } + ) + + resetEnvVars() + + os.Setenv("ACME_REREGISTER_ACCOUNT", strconv.FormatBool(reregisterAcc)) + os.Setenv("ACME_SERVER_URL", acmeServerURL) + os.Setenv("ACME_DNS_CHALLENGE_PROVIDER", dnsChallengeProvider) + os.Setenv("ACME_DNS_PROPAGATION_REQUIREMENT", strconv.FormatBool(dnsPropagationReq)) + os.Setenv("VAULT_APPROLE_ROLE_ID", vaultRoleID) + os.Setenv("VAULT_APPROLE_SECRET_ID", vaultSecretID) + os.Setenv("VAULT_KV_STORAGE_PATH", vaultKVStorePath) + os.Setenv("LOG_FORMAT", logFormat) + os.Setenv("LOG_LEVEL", logLevel) + os.Setenv("DNS_ADDRESS", dnsAddress) + os.Setenv("ENVIRONMENT", environment) + os.Setenv("CERTIFICATOR_RENEW_BEFORE_DAYS", strconv.Itoa(renewBeforeDays)) + os.Setenv("CERTIFICATOR_DOMAINS_LIST", "mydomain.com,www.mydomain.com,example.com") + + conf, err := LoadConfig() + testutil.Ok(t, err) + testutil.Equals(t, expectedConf, conf) +} + func resetEnvVars() { // Set required env vars os.Setenv("ACME_ACCOUNT_EMAIL", "test@test.com")