Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(server): load secrets from file instead of env var #972

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/server/knexfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { packageRoot } = require('./bootstrap')
const fs = require('fs')
const path = require('path')
const { isTestEnv } = require('@/modules/shared/helpers/envHelper')
const { getPostgresUrl } = require('@/modules/shared/helpers/secretsHelper')

function walk(dir) {
let results = []
Expand Down Expand Up @@ -42,11 +43,11 @@ let connectionUri
if (env.POSTGRES_USER && env.POSTGRES_PASSWORD) {
connectionUri = `postgres://${encodeURIComponent(
env.POSTGRES_USER
)}:${encodeURIComponent(env.POSTGRES_PASSWORD)}@${
env.POSTGRES_URL
}/${encodeURIComponent(env.POSTGRES_DB)}`
)}:${encodeURIComponent(
env.POSTGRES_PASSWORD
)}@${getPostgresUrl()}/${encodeURIComponent(env.POSTGRES_DB)}`
} else {
connectionUri = env.POSTGRES_URL
connectionUri = getPostgresUrl()
}

// NOTE: fixes time pagination, breaks graphql DateTime parsing :/
Expand Down
9 changes: 6 additions & 3 deletions packages/server/modules/auth/strategies.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use strict'

const ExpressSession = require('express-session')
const RedisStore = require('connect-redis')(ExpressSession)
const passport = require('passport')

const sentry = require('@/logging/sentryHelper')
const { createAuthorizationCode } = require('./services/apps')
const { isSSLServer, getRedisUrl } = require('@/modules/shared/helpers/envHelper')
const {
isSSLServer,
getRedisUrl,
getSessionSecret
} = require('@/modules/shared/helpers/envHelper')
const { authLogger } = require('@/logging/logging')
const { createRedisClient } = require('@/modules/shared/redis/redis')

Expand All @@ -24,7 +27,7 @@ module.exports = async (app) => {
const redisClient = createRedisClient(getRedisUrl())
const session = ExpressSession({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
secret: getSessionSecret(),
saveUninitialized: false,
resave: false,
cookie: {
Expand Down
3 changes: 2 additions & 1 deletion packages/server/modules/auth/strategies/azure-ad.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
const { passportAuthenticate } = require('@/modules/auth/services/passportService')
const { logger } = require('@/logging/logging')
const { UserInputError } = require('@/modules/core/errors/userinput')
const { getAzureADClientSecret } = require('@/modules/shared/helpers/secretsHelper')

module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const strategy = new OIDCStrategy(
Expand All @@ -28,7 +29,7 @@ module.exports = async (app, session, sessionStorage, finalizeAuth) => {
process.env.CANONICAL_URL
).toString(),
allowHttpForRedirectUrl: true,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
clientSecret: getAzureADClientSecret(),
scope: ['profile', 'email', 'openid'],
loggingLevel: process.env.NODE_ENV === 'development' ? 'info' : 'error',
passReqToCallback: true
Expand Down
3 changes: 2 additions & 1 deletion packages/server/modules/auth/strategies/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
const { passportAuthenticate } = require('@/modules/auth/services/passportService')
const { logger } = require('@/logging/logging')
const { UserInputError } = require('@/modules/core/errors/userinput')
const { getGitHubClientSecret } = require('@/modules/shared/helpers/secretsHelper')

module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const strategy = {
Expand All @@ -28,7 +29,7 @@ module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const myStrategy = new GithubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
clientSecret: getGitHubClientSecret(),
callbackURL: new URL(strategy.callbackUrl, process.env.CANONICAL_URL).toString(),
scope: ['profile', 'user:email'],
passReqToCallback: true
Expand Down
3 changes: 2 additions & 1 deletion packages/server/modules/auth/strategies/google.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
const { passportAuthenticate } = require('@/modules/auth/services/passportService')
const { logger } = require('@/logging/logging')
const { UserInputError } = require('@/modules/core/errors/userinput')
const { getGoogleClientSecret } = require('@/modules/shared/helpers/secretsHelper')

module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const strategy = {
Expand All @@ -26,7 +27,7 @@ module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const myStrategy = new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
clientSecret: getGoogleClientSecret(),
callbackURL: strategy.callbackUrl,
scope: ['profile', 'email'],
passReqToCallback: true
Expand Down
5 changes: 2 additions & 3 deletions packages/server/modules/blobstorage/objectStorage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { logger } = require('@/logging/logging')
const { NotFoundError } = require('@/modules/shared/errors')
const { getS3SecretKey } = require('@/modules/shared/helpers/secretsHelper')
const {
S3Client,
GetObjectCommand,
Expand All @@ -16,13 +17,11 @@ const getS3Config = () => {
if (!s3Config) {
if (!process.env.S3_ACCESS_KEY)
throw new Error('Config value S3_ACCESS_KEY is missing')
if (!process.env.S3_SECRET_KEY)
throw new Error('Config value S3_SECRET_KEY is missing')
if (!process.env.S3_ENDPOINT) throw new Error('Config value S3_ENDPOINT is missing')
s3Config = {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY
secretAccessKey: getS3SecretKey()
},
endpoint: process.env.S3_ENDPOINT,
forcePathStyle: true,
Expand Down
63 changes: 63 additions & 0 deletions packages/server/modules/emails/utils/transporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const debug = require('debug')('speckle')

const { getEmailPassword } = require('@/modules/shared/helpers/secretsHelper')
const nodemailer = require('nodemailer')
const modulesDebug = debug.extend('modules')
const errorDebug = debug.extend('errors')

/** @type {import('nodemailer').Transporter | undefined} */
let transporter = undefined

const createJsonEchoTransporter = () =>
nodemailer.createTransport({
jsonTransport: true
})

const initSmtpTransporter = async () => {
try {
const smtpTransporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT || 587,
secure: process.env.EMAIL_SECURE === 'true',
auth: {
user: process.env.EMAIL_USERNAME,
pass: getEmailPassword()
}
})
await smtpTransporter.verify()
return smtpTransporter
} catch (e) {
errorDebug('📧 Email provider is misconfigured, check config variables.', e)
}
}

/**
* @returns {import('nodemailer').Transporter | undefined}
*/
async function initializeTransporter() {
let newTransporter = undefined

if (process.env.NODE_ENV === 'test') newTransporter = createJsonEchoTransporter()
if (process.env.EMAIL === 'true') newTransporter = await initSmtpTransporter()

if (!newTransporter) {
modulesDebug(
'📧 Email provider is not configured. Server functionality will be limited.'
)
}

transporter = newTransporter
return newTransporter
}

/**
* @returns {import('nodemailer').Transporter | undefined}
*/
function getTransporter() {
return transporter
}

module.exports = {
initializeTransporter,
getTransporter
}
6 changes: 6 additions & 0 deletions packages/server/modules/shared/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export class MisconfiguredEnvironmentError extends BaseError {
'An error occurred due to the server environment being misconfigured'
}

export class MisconfiguredSecretError extends BaseError {
static code = 'MISONFIGURED_SECRET_ERROR'
static defaultMessage =
'An error occurred due to the server secret file being misconfigured'
}

export class UninitializedResourceAccessError extends BaseError {
static code = 'UNINITIALIZED_RESOURCE_ACCESS_ERROR'
static defaultMessage = 'Attempted to use uninitialized resources'
Expand Down
75 changes: 75 additions & 0 deletions packages/server/modules/shared/helpers/secretsHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { MisconfiguredSecretError } from '@/modules/shared/errors'
import { existsSync, readFile } from 'fs'
import { join } from 'path'

let redisUrl: string
let postgresUrl: string
let sessionSecret: string
let emailPassword: string
let s3SecretKey: string
let googleClientSecret: string
let githubClientSecret: string
let azureADClientSecret: string
let apolloKey: string

export function getRedisUrl(): string {
if (!redisUrl) redisUrl = getSecret('redis_url')
return redisUrl
}

export function getPostgresUrl(): string {
if (!postgresUrl) postgresUrl = getSecret('postgres_url')
return postgresUrl
}

export function getSessionSecret(): string {
if (!sessionSecret) sessionSecret = getSecret('session_secret')
return sessionSecret
}

export function getEmailPassword(): string {
if (!emailPassword) emailPassword = getSecret('email_password')
return emailPassword
}

export function getS3SecretKey(): string {
if (!s3SecretKey) s3SecretKey = getSecret('s3_secret_key')
return s3SecretKey
}

export function getGoogleClientSecret(): string {
if (!googleClientSecret) googleClientSecret = getSecret('google_client_secret')
return googleClientSecret
}

export function getGitHubClientSecret(): string {
if (!githubClientSecret) githubClientSecret = getSecret('github_client_secret')
return githubClientSecret
}

export function getAzureADClientSecret(): string {
if (!azureADClientSecret) azureADClientSecret = getSecret('azure_ad_client_secret')
return azureADClientSecret
}

export function getApolloKey(): string {
if (!apolloKey) apolloKey = getSecret('apollo_key')
return apolloKey
}

function getSecret(secretName: string): string {
const secretPath = join('/etc/secrets', secretName)
Copy link
Contributor

@fabis94 fabis94 Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(How?) Will this work on local development environments? Also I'm wondering if this shouldn't be an infra only solution (e.g. some cloud feature that stores some env vars separately, but when they reach the app theyre just env vars), not something that changes how our app reads environment config values? Just considering that this seems like a security feature only relevant for the prod env

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for challenging this, it led to a better solution.
To summarise what we discussed for future reference; the file won't exist on a dev environment, so this will fallback to using environment variables. It is backwards-compatible with our current dev & test setups.
On production the files would exist, so we wouldn't need the secrets in environment variables (secrets in env vars are relatively insecure).

if (existsSync(secretPath)) {
readFile(secretPath, { encoding: 'utf-8' }, function (err, data) {
if (!err) return data
})
}

if (process.env[secretName.toUpperCase()]) {
return process.env[secretName.toUpperCase()] ?? ''
}

throw new MisconfiguredSecretError(
`Neither ${secretPath} or ${secretName.toUpperCase()} env var were configured`
)
}
Loading