From d2f4ed93df5e1866c5389aa2a687a6bc3c944b1d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= <fred@miniflux.net>
Date: Mon, 29 Jun 2020 20:49:05 -0700
Subject: [PATCH] Add support for secret keys exposed as a file

Secret keys are often exposed as a file in containerized environments.
---
 cli/create_admin.go |  5 +++--
 config/options.go   | 16 ++++++++++++++++
 config/parser.go    | 32 ++++++++++++++++++++++++++++++++
 miniflux.1          | 22 ++++++++++++++++++++--
 4 files changed, 71 insertions(+), 4 deletions(-)

diff --git a/cli/create_admin.go b/cli/create_admin.go
index 35f0c4a5..8308c520 100644
--- a/cli/create_admin.go
+++ b/cli/create_admin.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"os"
 
+	"miniflux.app/config"
 	"miniflux.app/logger"
 	"miniflux.app/model"
 	"miniflux.app/storage"
@@ -15,8 +16,8 @@ import (
 
 func createAdmin(store *storage.Storage) {
 	user := model.NewUser()
-	user.Username = os.Getenv("ADMIN_USERNAME")
-	user.Password = os.Getenv("ADMIN_PASSWORD")
+	user.Username = config.Opts.AdminUsername()
+	user.Password = config.Opts.AdminPassword()
 	user.IsAdmin = true
 
 	if user.Username == "" || user.Password == "" {
diff --git a/config/options.go b/config/options.go
index 513396a1..5fc6c36c 100644
--- a/config/options.go
+++ b/config/options.go
@@ -39,6 +39,8 @@ const (
 	defaultCleanupRemoveSessionsDays          = 30
 	defaultProxyImages                        = "http-only"
 	defaultCreateAdmin                        = false
+	defaultAdminUsername                      = ""
+	defaultAdminPassword                      = ""
 	defaultOAuth2UserCreation                 = false
 	defaultOAuth2ClientID                     = ""
 	defaultOAuth2ClientSecret                 = ""
@@ -82,6 +84,8 @@ type Options struct {
 	schedulerEntryFrequencyMaxInterval int
 	workerPoolSize                     int
 	createAdmin                        bool
+	adminUsername                      string
+	adminPassword                      string
 	proxyImages                        string
 	oauth2UserCreationAllowed          bool
 	oauth2ClientID                     string
@@ -302,6 +306,16 @@ func (o *Options) CreateAdmin() bool {
 	return o.createAdmin
 }
 
+// AdminUsername returns the admin username if defined.
+func (o *Options) AdminUsername() string {
+	return o.adminUsername
+}
+
+// AdminPassword returns the admin password if defined.
+func (o *Options) AdminPassword() string {
+	return o.adminPassword
+}
+
 // ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
 func (o *Options) ProxyImages() string {
 	return o.proxyImages
@@ -378,6 +392,8 @@ func (o *Options) String() string {
 	builder.WriteString(fmt.Sprintf("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL: %v\n", o.schedulerEntryFrequencyMinInterval))
 	builder.WriteString(fmt.Sprintf("PROXY_IMAGES: %v\n", o.proxyImages))
 	builder.WriteString(fmt.Sprintf("CREATE_ADMIN: %v\n", o.createAdmin))
+	builder.WriteString(fmt.Sprintf("ADMIN_USERNAME: %v\n", o.adminUsername))
+	builder.WriteString(fmt.Sprintf("ADMIN_PASSWORD: %v\n", o.adminPassword))
 	builder.WriteString(fmt.Sprintf("POCKET_CONSUMER_KEY: %v\n", o.pocketConsumerKey))
 	builder.WriteString(fmt.Sprintf("OAUTH2_USER_CREATION: %v\n", o.oauth2UserCreationAllowed))
 	builder.WriteString(fmt.Sprintf("OAUTH2_CLIENT_ID: %v\n", o.oauth2ClientID))
diff --git a/config/parser.go b/config/parser.go
index 810ce56b..77b74357 100644
--- a/config/parser.go
+++ b/config/parser.go
@@ -6,9 +6,11 @@ package config // import "miniflux.app/config"
 
 import (
 	"bufio"
+	"bytes"
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
 	url_parser "net/url"
 	"os"
 	"strconv"
@@ -88,6 +90,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
 			p.opts.listenAddr = parseString(value, defaultListenAddr)
 		case "DATABASE_URL":
 			p.opts.databaseURL = parseString(value, defaultDatabaseURL)
+		case "DATABASE_URL_FILE":
+			p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
 		case "DATABASE_MAX_CONNS":
 			p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
 		case "DATABASE_MIN_CONNS":
@@ -148,14 +152,28 @@ func (p *Parser) parseLines(lines []string) (err error) {
 			p.opts.proxyImages = parseString(value, defaultProxyImages)
 		case "CREATE_ADMIN":
 			p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
+		case "ADMIN_USERNAME":
+			p.opts.adminUsername = parseString(value, defaultAdminUsername)
+		case "ADMIN_USERNAME_FILE":
+			p.opts.adminUsername = readSecretFile(value, defaultAdminUsername)
+		case "ADMIN_PASSWORD":
+			p.opts.adminPassword = parseString(value, defaultAdminPassword)
+		case "ADMIN_PASSWORD_FILE":
+			p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
 		case "POCKET_CONSUMER_KEY":
 			p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
+		case "POCKET_CONSUMER_KEY_FILE":
+			p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey)
 		case "OAUTH2_USER_CREATION":
 			p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
 		case "OAUTH2_CLIENT_ID":
 			p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
+		case "OAUTH2_CLIENT_ID_FILE":
+			p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
 		case "OAUTH2_CLIENT_SECRET":
 			p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
+		case "OAUTH2_CLIENT_SECRET_FILE":
+			p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
 		case "OAUTH2_REDIRECT_URL":
 			p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
 		case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
@@ -235,3 +253,17 @@ func parseString(value string, fallback string) string {
 	}
 	return value
 }
+
+func readSecretFile(filename, fallback string) string {
+	data, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return fallback
+	}
+
+	value := string(bytes.TrimSpace(data))
+	if value == "" {
+		return fallback
+	}
+
+	return value
+}
diff --git a/miniflux.1 b/miniflux.1
index fc1faa54..2f57f098 100644
--- a/miniflux.1
+++ b/miniflux.1
@@ -124,6 +124,9 @@ Postgresql connection parameters\&.
 .br
 Default is "user=postgres password=postgres dbname=miniflux2 sslmode=disable"\&.
 .TP
+.B DATABASE_URL_FILE
+Path to a secret key exposed as a file, it should contain $DATABASE_URL value\&.
+.TP
 .B DATABASE_MAX_CONNS
 Maximum number of database connections (default is 20)\&.
 .TP
@@ -188,9 +191,15 @@ OAuth2 provider to use\&. Only google is supported\&.
 .B OAUTH2_CLIENT_ID
 OAuth2 client ID\&.
 .TP
+.B OAUTH2_CLIENT_ID_FILE
+Path to a secret key exposed as a file, it should contain $OAUTH2_CLIENT_ID value\&.
+.TP
 .B OAUTH2_CLIENT_SECRET
 OAuth2 client secret\&.
 .TP
+.B OAUTH2_CLIENT_SECRET_FILE
+Path to a secret key exposed as a file, it should contain $OAUTH2_CLIENT_SECRET value\&.
+.TP
 .B OAUTH2_REDIRECT_URL
 OAuth2 redirect URL\&.
 .TP
@@ -207,14 +216,23 @@ Set to 1 to run database migrations\&.
 Set to 1 to create an admin user from environment variables\&.
 .TP
 .B ADMIN_USERNAME
-Admin user login, used only if \fBCREATE_ADMIN\fR is enabled\&.
+Admin user login, used only if $CREATE_ADMIN is enabled\&.
+.TP
+.B ADMIN_USERNAME_FILE
+Path to a secret key exposed as a file, it should contain $ADMIN_USERNAME value\&.
 .TP
 .B ADMIN_PASSWORD
-Admin user password, used only if \fBCREATE_ADMIN\fR is enabled\&.
+Admin user password, used only if $CREATE_ADMIN is enabled\&.
+.TP
+.B ADMIN_PASSWORD_FILE
+Path to a secret key exposed as a file, it should contain $ADMIN_PASSWORD value\&.
 .TP
 .B POCKET_CONSUMER_KEY
 Pocket consumer API key for all users\&.
 .TP
+.B POCKET_CONSUMER_KEY_FILE
+Path to a secret key exposed as a file, it should contain $POCKET_CONSUMER_KEY value\&.
+.TP
 .B PROXY_IMAGES
 Avoids mixed content warnings for external images: http-only, all, or none\&.
 .br
-- 
GitLab