Skip to content

Commit

Permalink
add: allow reading secrets from files (#47)
Browse files Browse the repository at this point in the history
Pass the path to the file containing the secret to the service by
appending `_FILE` to the env var name which normally would contain the
secret directly.

If both env vars `SECRET` and `SECRET_FILE` are set, `SECRET` takes
precedence.

As the original way of supplying secrets is still supported, this is a
backwards compatible change.

## Why

Using the docker secrets feature results in files containing the secret
being placed into the container filesystem.
  • Loading branch information
mgoetzegb authored Nov 29, 2024
2 parents fb80b0a + b005aea commit 1e68e44
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 36 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.22.5-alpine AS builder
FROM golang:1.23.3-alpine AS builder
RUN apk add --no-cache make

# swagger docs generation will fail if cgo is used
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ For running the Notification Service outside of docker the latest version of [go

The service is configured via environment variables. Refer to the [Config](pkg/config/config.go) for the available options and their defaults.

The secret `DB_PASSWORD` can be also passed by file. Simply pass the path to the file containing the secret to the service by appending `_FILE` to the env var name, i.e. `DB_PASSWORD_FILE`. If the secret is supplied in both ways, the one directly passed by env var takes precedence.

## Running

> The following instructions are targeted at openSight developers. As end user the services should be run in orchestration with the other openSight services, which is not in the scope of this readme.
Expand Down
20 changes: 17 additions & 3 deletions cmd/notification-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"os"
"os/signal"

"github.com/go-playground/validator"
"github.com/greenbone/opensight-notification-service/pkg/config"
"github.com/greenbone/opensight-notification-service/pkg/config/secretfiles"
"github.com/greenbone/opensight-notification-service/pkg/logging"
"github.com/greenbone/opensight-notification-service/pkg/repository"
"github.com/greenbone/opensight-notification-service/pkg/repository/notificationrepository"
Expand All @@ -20,22 +22,34 @@ import (
"github.com/greenbone/opensight-notification-service/pkg/web"
"github.com/greenbone/opensight-notification-service/pkg/web/healthcontroller"
"github.com/greenbone/opensight-notification-service/pkg/web/notificationcontroller"
"github.com/kelseyhightower/envconfig"

"github.com/rs/zerolog/log"
)

func main() {
config, err := config.ReadConfig()
var cfg config.Config
// Note: secrets can be passed directly by env var or via file
// if the same secret is supplied in both ways, the env var takes precedence
err := secretfiles.Read(&cfg)
if err != nil {
log.Fatal().Err(err).Msg("failed to read secrets from files")
}
err = envconfig.Process("", &cfg)
if err != nil {
log.Fatal().Err(err).Msg("failed to read config")
}
err = validator.New().Struct(cfg)
if err != nil {
log.Fatal().Err(err).Msg("invalid config")
}

err = logging.SetupLogger(config.LogLevel)
err = logging.SetupLogger(cfg.LogLevel)
if err != nil {
log.Fatal().Err(err).Msg("failed to set up logger")
}

check(run(config))
check(run(cfg))
}

func run(config config.Config) error {
Expand Down
5 changes: 4 additions & 1 deletion docker-compose.service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
services:
notification-service:
build: . # replace this line with `image: ghcr.io/greenbone/notification-service:<desired vibd docker image>` if you want to use an already built image instead of building one from the active working directory
secrets:
- PostgresPassword
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: postgres
DB_PASSWORD: $DB_PASSWORD
DB_PASSWORD_FILE: /run/secrets/PostgresPassword
DB_NAME: notification_service
DB_SSL_MODE: disable
LOG_LEVEL: debug
Expand All @@ -20,3 +22,4 @@ services:
depends_on:
postgres:
condition: service_healthy

8 changes: 7 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
services:
postgres:
image: postgres:16
secrets:
- PostgresPassword
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: $DB_PASSWORD
POSTGRES_PASSWORD_FILE: /run/secrets/PostgresPassword
POSTGRES_DB: notification_service
volumes:
- postgres-data:/var/lib/postgresql/data
Expand All @@ -24,3 +26,7 @@ volumes:

networks:
notification-service-net:

secrets:
PostgresPassword:
environment: DB_PASSWORD
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
module github.com/greenbone/opensight-notification-service

go 1.22.5
go 1.23.3

require (
github.com/gin-contrib/cors v1.7.2
github.com/gin-contrib/logger v1.2.0
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator v9.31.0+incompatible
github.com/golang-migrate/migrate/v4 v4.18.1
github.com/greenbone/opensight-golang-libraries v1.9.2
github.com/greenbone/opensight-golang-libraries v1.10.0
github.com/jmoiron/sqlx v1.4.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.47.0
github.com/stretchr/testify v1.10.0
Expand Down Expand Up @@ -76,6 +78,7 @@ require (
golang.org/x/text v0.20.0 // indirect
golang.org/x/tools v0.27.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/gorm v1.25.12 // indirect
Expand Down
14 changes: 10 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
Expand All @@ -84,8 +86,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/greenbone/opensight-golang-libraries v1.9.2 h1:vNhli1NOqN1B0bAYe+6CrHpTwBYFJgS8/kflCsCGdo8=
github.com/greenbone/opensight-golang-libraries v1.9.2/go.mod h1:2W1xxkzmUdLGkQa65kCcM7TZY2FR3f7C2bEGhuh43Vg=
github.com/greenbone/opensight-golang-libraries v1.10.0 h1:NFVMEnOmtyQVfNyyQ2FhqwvpiI0mdrC6IHk4fqwrMbw=
github.com/greenbone/opensight-golang-libraries v1.10.0/go.mod h1:7RJ/YKVy6Kjl1FBD4EqSzbZk6O6QdoeTEWJBy/XQS+Y=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
Expand All @@ -111,6 +113,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
Expand Down Expand Up @@ -270,13 +274,15 @@ google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/postgres v1.5.10 h1:7Lggqempgy496c0WfHXsYWxk3Th+ZcW66/21QhVFdeE=
gorm.io/driver/postgres v1.5.10/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
40 changes: 16 additions & 24 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,29 @@ package config

import (
"time"

"github.com/greenbone/opensight-golang-libraries/pkg/configReader"
)

// Note: The `envconfig` entries of fields in nested structs are prefixed by the `envconfig` entry of the struct
// I.e. the env var in {INNER1:{INNER2:{FIELD1:"foo"}}} for FIELD1 is `INNER1_INNER2_FIELD1`

type Config struct {
Http Http
Database Database
LogLevel string `viperEnv:"LOG_LEVEL" default:"info"`
Http Http `envconfig:"HTTP"`
Database Database `envconfig:"DB"`
LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
}

type Http struct {
Port int `validate:"required,min=1,max=65535" viperEnv:"HTTP_PORT" default:"8085"`
ReadTimeout time.Duration `viperEnv:"HTTP_READ_TIMEOUT" default:"10s"`
WriteTimeout time.Duration `viperEnv:"HTTP_WRITE_TIMEOUT" default:"60s"`
IdleTimeout time.Duration `viperEnv:"HTTP_IDLE_TIMEOUT" default:"60s"`
Port int `validate:"required,min=1,max=65535" envconfig:"PORT" default:"8085"`
ReadTimeout time.Duration `envconfig:"READ_TIMEOUT" default:"10s"`
WriteTimeout time.Duration `envconfig:"WRITE_TIMEOUT" default:"60s"`
IdleTimeout time.Duration `envconfig:"IDLE_TIMEOUT" default:"60s"`
}

type Database struct {
Host string `viperEnv:"DB_HOST" default:"localhost"`
Port int `validate:"required,min=1,max=65535" viperEnv:"DB_PORT" default:"5432"`
User string `validate:"required" viperEnv:"DB_USERNAME"`
Password string `validate:"required" viperEnv:"DB_PASSWORD"`
DBName string `validate:"required" viperEnv:"DB_NAME"`
SSLMode string `viperEnv:"DB_SSL_MODE" default:"require"`
}

func ReadConfig() (Config, error) {
var config Config
_, err := configReader.ReadEnvVarsIntoStruct(&config)
if err != nil {
return config, err
}
return config, nil
Host string `envconfig:"HOST" default:"localhost"`
Port int `validate:"required,min=1,max=65535" envconfig:"PORT" default:"5432"`
User string `validate:"required" envconfig:"USERNAME"`
Password string `validate:"required" envconfig:"PASSWORD"`
DBName string `validate:"required" envconfig:"NAME"`
SSLMode string `envconfig:"SSL_MODE" default:"require"`
}
22 changes: 22 additions & 0 deletions pkg/config/secretfiles/secret_files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2024 Greenbone AG <https://greenbone.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package secretfiles

import (
"github.com/greenbone/opensight-golang-libraries/pkg/secretfiles"
"github.com/greenbone/opensight-notification-service/pkg/config"
)

const (
dbPasswordPathEnvVar = "DB_PASSWORD_FILE"
)

// Read takes the filepaths from environment variables and parses the content
// into the respective secret inside the passed config.
// A failure can have side effects on the passed config, so error from this function
// should be treated as fatal.
func Read(cfg *config.Config) (err error) {
return secretfiles.ReadSecret(dbPasswordPathEnvVar, &cfg.Database.Password)
}
65 changes: 65 additions & 0 deletions pkg/config/secretfiles/secret_files_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2024 Greenbone AG <https://greenbone.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package secretfiles

import (
"os"
"testing"

"github.com/greenbone/opensight-notification-service/pkg/config"
"github.com/stretchr/testify/require"
)

func TestRead(t *testing.T) {
// create files containing secrets
tempDir := t.TempDir()
err := os.WriteFile(tempDir+"/db_password", []byte(" db_password \n\n\t"), 0644)
require.NoError(t, err)

tests := map[string]struct {
envVars map[string]string
inputConfig config.Config
wantConfig config.Config
wantErr bool
}{
"read all secrets from files": {
inputConfig: config.Config{},
envVars: map[string]string{
"DB_PASSWORD_FILE": tempDir + "/db_password",
},
wantConfig: config.Config{
Database: config.Database{
Password: `db_password`,
},
},
wantErr: false,
},
"failure with invalid path": {
inputConfig: config.Config{},
envVars: map[string]string{
"DB_PASSWORD_FILE": "/invalid/path",
},
wantErr: true,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
// set the environment variables
for key, value := range tt.envVars {
err := os.Setenv(key, value)
require.NoError(t, err)
}

err := Read(&tt.inputConfig)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.wantConfig, tt.inputConfig)
}
})
}
}

0 comments on commit 1e68e44

Please sign in to comment.