diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index ceaf5542c8..0c61bc925b 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -39,7 +39,7 @@ func run(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("set up account: %w", err) } - _, err = dotenv.Load(dotenv.BaseFilePrefix) + _, err = dotenv.Load(cmd.String(flags.FlgEnvFile)) if err != nil { return fmt.Errorf("set up environment: %w", err) } diff --git a/cmd/internal/configuration/configuration.go b/cmd/internal/configuration/configuration.go index 7caa1e7462..fb630fc49a 100644 --- a/cmd/internal/configuration/configuration.go +++ b/cmd/internal/configuration/configuration.go @@ -69,6 +69,7 @@ type DNSChallenge struct { Propagation *Propagation `yaml:"propagation,omitempty"` DNSTimeout int `yaml:"dnsTimeout,omitempty"` Resolvers []string `yaml:"resolvers,omitempty"` + EnvFile string `yaml:"envFile,omitempty"` } type DNSPersistChallenge struct { diff --git a/cmd/internal/configuration/testdata/reference.yml b/cmd/internal/configuration/testdata/reference.yml index 74e287895c..0a8b3a1da3 100644 --- a/cmd/internal/configuration/testdata/reference.yml +++ b/cmd/internal/configuration/testdata/reference.yml @@ -1,7 +1,7 @@ # This is not a valid configuration file. -# This must be moved/used as a reference inside the documentation. +# Don't use it. -storage: /tmp/lego +storage: /tmp/lego/ networkStack: ipv6only userAgent: foo @@ -40,6 +40,7 @@ challenges: three: dns: provider: cloudflare + envFile: /tmp/secrets/.env.cf propagation: disableAuthoritativeNameservers: true disableRecursiveNameservers: true diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index f59e5e25e0..52f387525c 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -270,7 +270,9 @@ func createACMEClientFlags() []cli.Flag { } func createChallengesFlags() []cli.Flag { - var flags []cli.Flag + flags := []cli.Flag{ + CreateEnvFileFlag(), + } flags = append(flags, createHTTPChallengeFlags()...) flags = append(flags, createTLSChallengeFlags()...) @@ -765,6 +767,15 @@ func CreatePathFlag(forceCreation bool) cli.Flag { } } +func CreateEnvFileFlag() cli.Flag { + return &cli.StringFlag{ + Category: categoryStorage, + Name: FlgEnvFile, + Sources: cli.EnvVars(toEnvName(FlgEnvFile)), + Usage: "The path to the dotenv file.", + } +} + func createLogFlags() []cli.Flag { return []cli.Flag{ &cli.StringFlag{ diff --git a/cmd/internal/flags/names.go b/cmd/internal/flags/names.go index 1f5cf8e072..5fd7d08ade 100644 --- a/cmd/internal/flags/names.go +++ b/cmd/internal/flags/names.go @@ -59,6 +59,7 @@ const ( // Flag names related to the storage. const ( FlgPath = "path" + FlgEnvFile = "env-file" FlgPEM = "pem" FlgPFX = "pfx" FlgPFXPass = "pfx.password" diff --git a/cmd/internal/root/process.go b/cmd/internal/root/process.go index 36235f8669..8b15ef0b1f 100644 --- a/cmd/internal/root/process.go +++ b/cmd/internal/root/process.go @@ -78,7 +78,7 @@ func process(ctx context.Context, cfg *configuration.Configuration) error { func processChallenges(ctx context.Context, lazyClient lzSetUp, chlgNode *configuration.ChallengeNode, store *storage.Storage, hookManager *hook.Manager, networkStack challenge.NetworkStack) error { if chlgNode.DNS != nil { - cleanUp, err := dotenv.Load(dotenv.BaseFilePrefix, dotenv.BaseFilePrefix+"."+chlgNode.ID) + cleanUp, err := dotenv.Load(chlgNode.DNS.EnvFile) defer cleanUp() diff --git a/docs/content/obtain/dns01.md b/docs/content/obtain/dns01.md index 270ada686f..b73256f457 100644 --- a/docs/content/obtain/dns01.md +++ b/docs/content/obtain/dns01.md @@ -72,19 +72,38 @@ CLOUDFLARE_API_KEY='yourprivatecloudflareapikey' \ You can also use a dotenv file. -When using `lego run`, the file `.env` is automatically loaded. +When using `lego run`, you can pass the path to the dotenv file with the `--env-file` flag. -When using `lego`, the environment variables are loaded from the `.env` file, and from the file `.env.` where `` is the name defined in the configuration file as the challenge name (not the provider name). +When using `lego`, the environment variables are loaded from the file defined by `envFile` in the configuration file for the DNS provider. -The environment variables defined in the file `.env.` overrides the environment variables defined in file `.env` ("merge"). +{{< tabs >}} +{{% tab title=".lego.yml" %}} -For example, in the previous example with the file configuration, you can create a file `.env.cf` to define the credentials for the Cloudflare provider insead of defining them in the environment variables. +```yaml +challenges: + cf: + dns: + provider: cloudflare + envFile: .env.cf -```ini +certificates: + foo: + domains: + - example.com + - '*.example.com' +``` + +{{% /tab %}} +{{% tab title=".env.cf" %}} + +```dotenv CLOUDFLARE_EMAIL=you@example.com CLOUDFLARE_API_KEY=yourprivatecloudflareapikey ``` +{{% /tab %}} +{{< /tabs >}} + ## Tips {{% notice title="For a zone that has multiple SOAs" icon="info-circle" %}} diff --git a/docs/content/references/ref-file/_index.md b/docs/content/references/ref-file/_index.md index ec54f38dc3..5fc03ec2c1 100644 --- a/docs/content/references/ref-file/_index.md +++ b/docs/content/references/ref-file/_index.md @@ -22,7 +22,7 @@ The configuration file can be validated with the JSON Schema: [lego.jsonschema.j # Path to the directory to use for storing the data. # # Default: ./lego -storage: /tmp/lego +storage: /tmp/lego/ # The network stack to use. # It can be: @@ -259,6 +259,11 @@ challenges: # Required. provider: cloudflare + # The path to the dotenv file containing the credentials. + # + # Optional. + envFile: /tmp/secrets/.env + # The configuration related to propagation check. # # Optional. diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index eceb3f37c9..7095b56abc 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -139,6 +139,7 @@ OPTIONS: --account-id string Account identifier (The email is used if the account ID is undefined). [$LEGO_ACCOUNT_ID] --cert.name string, -c string The certificate ID/Name, used to store and retrieve a certificate. By default, it uses the first domain name. [$LEGO_CERT_NAME] + --env-file string The path to the dotenv file. [$LEGO_ENV_FILE] --path string Directory to use for storing the data. [$LEGO_PATH] --pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together. [$LEGO_PEM] --pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX] diff --git a/docs/static/lego.jsonschema.json b/docs/static/lego.jsonschema.json index 41b5f8a371..6a32008a15 100644 --- a/docs/static/lego.jsonschema.json +++ b/docs/static/lego.jsonschema.json @@ -142,6 +142,10 @@ }, "propagation": { "$ref": "#/definitions/propagationSettings" + }, + "envFile": { + "type": "string", + "default": "" } } }, diff --git a/e2e/configuration/challenges_test.go b/e2e/configuration/challenges_test.go index 1367593174..040db7a900 100644 --- a/e2e/configuration/challenges_test.go +++ b/e2e/configuration/challenges_test.go @@ -19,7 +19,6 @@ var load = loader.EnvLoader{ }, LegoOptions: []string{ "LEGO_CA_CERTIFICATES=../fixtures/certs/pebble.minica.pem", - "12b79c45_2153_4e99_9518_67b3350d878b=./fixtures", "LEGO_DEBUG_ACME_HTTP_CLIENT=1", }, ChallSrv: &loader.CmdOption{ diff --git a/e2e/configuration/fixtures/.env.mychallenge b/e2e/configuration/fixtures/.env.lego.mychallenge similarity index 100% rename from e2e/configuration/fixtures/.env.mychallenge rename to e2e/configuration/fixtures/.env.lego.mychallenge diff --git a/e2e/configuration/fixtures/lego_dns-explicit.yml b/e2e/configuration/fixtures/lego_dns-explicit.yml index 3df1f6e902..6b9aa53d6b 100644 --- a/e2e/configuration/fixtures/lego_dns-explicit.yml +++ b/e2e/configuration/fixtures/lego_dns-explicit.yml @@ -6,6 +6,7 @@ challenges: wait: 500ms resolvers: - :8853 + envFile: ./fixtures/.env.lego.mychallenge certificates: 'dns.localhost': diff --git a/e2e/configuration/fixtures/lego_dns-simple.yml b/e2e/configuration/fixtures/lego_dns-simple.yml index 38fabc2570..7744c5046d 100644 --- a/e2e/configuration/fixtures/lego_dns-simple.yml +++ b/e2e/configuration/fixtures/lego_dns-simple.yml @@ -6,6 +6,7 @@ challenges: wait: 500ms resolvers: - :8853 + envFile: ./fixtures/.env.lego.mychallenge certificates: 'dns.localhost': diff --git a/e2e/dnschallenge/challenges_test.go b/e2e/dnschallenge/challenges_test.go index f6a0755669..1f01a3460d 100644 --- a/e2e/dnschallenge/challenges_test.go +++ b/e2e/dnschallenge/challenges_test.go @@ -20,7 +20,6 @@ var load = loader.EnvLoader{ }, LegoOptions: []string{ "LEGO_CA_CERTIFICATES=../fixtures/certs/pebble.minica.pem", - "12b79c45_2153_4e99_9518_67b3350d878b=./fixtures", "LEGO_DEBUG_ACME_HTTP_CLIENT=1", }, ChallSrv: &loader.CmdOption{ diff --git a/e2e/dnschallenge/dns_challenge_test.go b/e2e/dnschallenge/dns_challenge_test.go index 4f690f9d40..1aafcda456 100644 --- a/e2e/dnschallenge/dns_challenge_test.go +++ b/e2e/dnschallenge/dns_challenge_test.go @@ -34,6 +34,7 @@ func TestChallengeDNS_Run(t *testing.T) { "--dns", "exec", "--dns.resolvers", ":8553", "--dns.propagation.wait", "0", + "--env-file", "./fixtures/.env", "-s", caDirectory, "-d", testDomain2, "-d", testDomain1, diff --git a/internal/dotenv/loader.go b/internal/dotenv/loader.go index d140d27e92..0f86e0c9d3 100644 --- a/internal/dotenv/loader.go +++ b/internal/dotenv/loader.go @@ -4,25 +4,15 @@ import ( "log/slog" "maps" "os" - "path/filepath" + "strings" "github.com/go-acme/lego/v5/log" "github.com/joho/godotenv" ) -const BaseFilePrefix = ".env" - func Load(filenames ...string) (func(), error) { - // ONLY FOR TESTING PURPOSE: DON'T USE IT!! - prefix, ok := os.LookupEnv("12b79c45_2153_4e99_9518_67b3350d878b") - if ok { - var prefixed []string - - for _, filename := range filenames { - prefixed = append(prefixed, filepath.Join(prefix, filename)) - } - - filenames = prefixed + if len(filenames) == 0 { + return noopCleanUp, nil } envs, err := read(filenames) @@ -65,10 +55,14 @@ func read(filenames []string) (map[string]string, error) { envMap := make(map[string]string) for _, filename := range filenames { + if strings.TrimSpace(filename) == "" { + continue + } + _, err := os.Stat(filename) if err != nil { if os.IsNotExist(err) { - log.Debug("Environment file not found", slog.String("filename", filename)) + log.Info("Environment file not found", slog.String("filename", filename)) continue } diff --git a/internal/dotenv/loader_test.go b/internal/dotenv/loader_test.go index ce22621c24..a9619e2c3e 100644 --- a/internal/dotenv/loader_test.go +++ b/internal/dotenv/loader_test.go @@ -22,16 +22,16 @@ func TestLoad(t *testing.T) { }, { desc: "non-existing file", - filenames: []string{filepath.Join("testdata", BaseFilePrefix+".non-existing")}, + filenames: []string{filepath.Join("testdata", ".env.lego.non-existing")}, }, { desc: "simple", - filenames: []string{filepath.Join("testdata", BaseFilePrefix)}, + filenames: []string{filepath.Join("testdata", ".env.lego.bar")}, expected: []string{"LEGO_TEST_ENV_A=aGlobal", "LEGO_TEST_ENV_B=bGlobal"}, }, { desc: "multiple files", - filenames: []string{filepath.Join("testdata", BaseFilePrefix), filepath.Join("testdata", BaseFilePrefix+".foo")}, + filenames: []string{filepath.Join("testdata", ".env.lego.bar"), filepath.Join("testdata", ".env.lego.foo")}, expected: []string{"LEGO_TEST_ENV_A=aLocal", "LEGO_TEST_ENV_B=bGlobal"}, }, } diff --git a/internal/dotenv/testdata/.env b/internal/dotenv/testdata/.env.lego.bar similarity index 100% rename from internal/dotenv/testdata/.env rename to internal/dotenv/testdata/.env.lego.bar diff --git a/internal/dotenv/testdata/.env.foo b/internal/dotenv/testdata/.env.lego.foo similarity index 100% rename from internal/dotenv/testdata/.env.foo rename to internal/dotenv/testdata/.env.lego.foo