Merge pull request #1234 from dod-ccpo/kv-flask-config

Use Key Vault for Flask application config
This commit is contained in:
dandds 2019-12-10 10:32:52 -05:00 committed by GitHub
commit d9c79b9b58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 275 additions and 99 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null
},
"generated_at": "2019-12-03T19:44:47Z",
"generated_at": "2019-12-05T17:54:05Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -98,7 +98,7 @@
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_secret": false,
"is_verified": false,
"line_number": 21,
"line_number": 29,
"type": "Secret Keyword"
}
],

View File

@ -168,12 +168,7 @@ Testing file uploads and downloads locally requires a few configuration options.
In the flask config (`config/base.ini`, perhaps):
```
CSP=<aws | azure | mock>
AWS_REGION_NAME=""
AWS_ACCESS_KEY=""
AWS_SECRET_KEY=""
AWS_BUCKET_NAME=""
CSP=< azure | mock>
AZURE_STORAGE_KEY=""
AZURE_ACCOUNT_NAME=""
@ -183,7 +178,7 @@ AZURE_TO_BUCKET_NAME=""
There are also some build-time configuration that are used by parcel. Add these to `.env.local`, and run `rm -r .cache/` before running `yarn build`:
```
CLOUD_PROVIDER=<aws | azure | mock>
CLOUD_PROVIDER=<azure | mock>
AZURE_ACCOUNT_NAME=""
AZURE_CONTAINER_NAME=""
```
@ -223,6 +218,9 @@ To generate coverage reports for the Javascript tests:
## Configuration
- `ASSETS_URL`: URL to host which serves static assets (such as a CDN).
- `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account
- `AZURE_STORAGE_KEY`: A valid secret key for the Azure blob storage account
- `AZURE_TO_BUCKET_NAME`: The Azure blob storage container name for task order uploads
- `BLOB_STORAGE_URL`: URL to Azure blob storage container.
- `CAC_URL`: URL for the CAC authentication route.
- `CA_CHAIN`: Path to the CA chain file.
@ -238,6 +236,11 @@ To generate coverage reports for the Javascript tests:
- `ENVIRONMENT`: String specifying the current environment. Acceptable values: "dev", "prod".
- `LIMIT_CONCURRENT_SESSIONS`: Boolean specifying if users should be allowed only one active session at a time.
- `LOG_JSON`: Boolean specifying whether app should log in a json format.
- `MAIL_PASSWORD`: String. Password for the SMTP server.
- `MAIL_PORT`: Integer. Port to use on the SMTP server.
- `MAIL_SENDER`: String. Email address to send outgoing mail from.
- `MAIL_SERVER`: The SMTP host
- `MAIL_TLS`: Boolean. Use TLS to connect to the SMTP server.
- `PERMANENT_SESSION_LIFETIME`: Integer specifying how many seconds a user's session can stay valid for. https://flask.palletsprojects.com/en/1.1.x/config/#PERMANENT_SESSION_LIFETIME
- `PGDATABASE`: String specifying the name of the postgres database.
- `PGHOST`: String specifying the hostname of the postgres database.

View File

@ -200,23 +200,21 @@ def make_config(direct_config=None):
ENV_CONFIG_FILENAME = os.path.join(
os.path.dirname(__file__), "../config/", "{}.ini".format(ENV.lower())
)
OVERRIDE_CONFIG_FILENAME = os.getenv("OVERRIDE_CONFIG_FULLPATH")
OVERRIDE_CONFIG_DIRECTORY = os.getenv("OVERRIDE_CONFIG_DIRECTORY")
config = ConfigParser(allow_no_value=True)
config.optionxform = str
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
if OVERRIDE_CONFIG_FILENAME:
config_files.append(OVERRIDE_CONFIG_FILENAME)
# ENV_CONFIG will override values in BASE_CONFIG.
config.read(config_files)
if OVERRIDE_CONFIG_DIRECTORY:
apply_config_from_directory(OVERRIDE_CONFIG_DIRECTORY, config)
# Check for ENV variables as a final source of overrides
for confsetting in config.options("default"):
env_override = os.getenv(confsetting.upper())
if env_override:
config.set("default", confsetting, env_override)
apply_config_from_environment(config)
# override if a dictionary of options has been given
if direct_config:
@ -244,6 +242,36 @@ def make_config(direct_config=None):
return map_config(config)
def apply_config_from_directory(config_dir, config, section="default"):
"""
Loop files in a directory, check if the names correspond to
known config values, and apply the file contents as the value
for that setting if they do.
"""
for confsetting in os.listdir(config_dir):
if confsetting in config.options(section):
full_path = os.path.join(config_dir, confsetting)
with open(full_path, "r") as conf_file:
config.set(section, confsetting, conf_file.read().strip())
return config
def apply_config_from_environment(config, section="default"):
"""
Loops all the configuration settins in a given section of a
config object and checks whether those settings also exist as
environment variables. If so, it applies the environment
variables value as the new configuration setting value.
"""
for confsetting in config.options(section):
env_override = os.getenv(confsetting.upper())
if env_override:
config.set(section, confsetting, env_override)
return config
def make_redis(app, config):
r = redis.Redis.from_url(config["REDIS_URI"])
app.redis = r

View File

@ -1,5 +1,8 @@
[default]
ASSETS_URL
AZURE_ACCOUNT_NAME
AZURE_STORAGE_KEY
AZURE_TO_BUCKET_NAME
BLOB_STORAGE_URL=http://localhost:8000/
CAC_URL = http://localhost:8000/login-redirect
CA_CHAIN = ssl/server-certs/ca-chain.pem
@ -15,6 +18,11 @@ DISABLE_CRL_CHECK = false
ENVIRONMENT = dev
LIMIT_CONCURRENT_SESSIONS = false
LOG_JSON = false
MAIL_PASSWORD
MAIL_PORT
MAIL_SENDER
MAIL_SERVER
MAIL_TLS
PERMANENT_SESSION_LIFETIME = 1800
PGDATABASE = atat
PGHOST = localhost

View File

@ -37,35 +37,6 @@ If you are satisfied with the output from the diff, you can apply the new config
## Secrets and Configuration
### atst-overrides.ini
Production configuration values are provided to the ATAT Flask app by writing an `atst-overrides.ini` file to the running Docker container. This file is stored as a Kubernetes secret. It contains configuration information for the database connection, mailer, etc.
To update the configuration, you can do the following:
```
kubectl -n atat get secret atst-config-ini -o=jsonpath='{.data.override\.ini}' | base64 --decode > override.ini
```
This base64 decodes the secret and writes it to a local file called `override.ini`. Make any necessary config changes to that file.
To apply the new config, first delete the existing copy of the secret:
```
kubectl -n atat delete secret atst-config-ini
```
Then create a new copy of the secret from your updated copy:
```
kubectl -n atat create secret generic atst-config-ini --from-file=./override.ini
```
Notes:
- Be careful not to check the override.ini file into source control.
- Be careful not to overwrite one CSP cluster's config with the other's. This will break everything.
### nginx-htpasswd
If the site is running in dev mode, the `/login-dev` endpoint is available. This endpoint is protected by basic HTTP auth. To create a new password file, run:
@ -178,11 +149,32 @@ az keyvault secret set --vault-name <VAULT NAME> --name <NAME OF PARAM> --value
```
---
# Secrets Management
Secrets, keys, and certificates are managed from Azure Key Vault. These items are mounted into the containers at runtime using the FlexVol implementation described below.
The following are mounted into the NGINX container in the atst pod:
- The TLS certs for the site
- The DH parameter for TLS connections
These are mounted into every instance of the Flask application container (the atst container, the celery worker, etc.):
- The Azure storage key used to access blob storage (AZURE_STORAGE_KEY)
- The password for the SMTP server used to send mail (MAIL_PASSWORD)
- The Postgres database user password (PGPASSWORD)
- The Redis user password (REDIS_PASSWORD)
- The Flask secret key used for session signing and generating CSRF tokens (SECRET_KEY)
Secrets should be added to Key Vault with the following naming pattern: [branch/environment]-[all-caps config setting name]. Note that Key Vault does not support underscores. Substitute hyphens. For example, the config setting for the SMTP server password is MAIL_SERVER. The corresponding secret name in Key Vault is "master-MAIL-SERVER" for the credential used in the primary environment.These secrets are mounted into the containers via FlexVol.
To add or manage secrets, keys, and certificates in Key Vault, see the [documentation](https://docs.microsoft.com/en-us/azure/key-vault/quick-create-cli).
# Setting Up FlexVol for Secrets
## Preparing Azure Environment
A Key Vault will need to be created. Save it's full id (the full path) for use later.
A Key Vault will need to be created. Save its full id (the full path) for use later.
## Preparing Cluster

View File

@ -6,15 +6,28 @@ metadata:
namespace: atat
data:
ASSETS_URL: https://atat-cdn.azureedge.net/
AZURE_ACCOUNT_NAME: atat
AZURE_TO_BUCKET_NAME: task-order-pdfs
BLOB_STORAGE_URL: https://atat.blob.core.windows.net/
CAC_URL: https://auth-staging.atat.code.mil/login-redirect
CDN_ORIGIN: https://azure.atat.code.mil
CELERY_DEFAULT_QUEUE: celery-master
CSP: azure
DEBUG: 0
FLASK_ENV: master
LOG_JSON: "true"
OVERRIDE_CONFIG_FULLPATH: /opt/atat/atst/atst-overrides.ini
MAIL_PORT: 587
MAIL_SENDER: postmaster@atat.code.mil
MAIL_SERVER: smtp.mailgun.org
MAIL_TLS: "true"
OVERRIDE_CONFIG_DIRECTORY: /config
PGAPPNAME: atst
PGDATABASE: staging
PGHOST: atat-db.postgres.database.azure.com
PGPORT: 5432
PGSSLMODE: verify-full
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
PGUSER: atat_master@atat-db
REDIS_HOST: atat.redis.cache.windows.net:6380
REDIS_TLS: "true"
STATIC_URL: https://atat-cdn.azureedge.net/static/

View File

@ -5,9 +5,25 @@ metadata:
name: atst-worker-envvars
namespace: atat
data:
AZURE_ACCOUNT_NAME: atat
AZURE_TO_BUCKET_NAME: task-order-pdfs
CAC_URL: https://auth-staging.atat.code.mil/login-redirect
CELERY_DEFAULT_QUEUE: celery-master
DISABLE_CRL_CHECK: "True"
DEBUG: 0
DISABLE_CRL_CHECK: "true"
MAIL_PORT: 587
MAIL_SENDER: postmaster@atat.code.mil
MAIL_SERVER: smtp.mailgun.org
MAIL_TLS: "true"
OVERRIDE_CONFIG_DIRECTORY: /config
PGAPPNAME: atst
PGDATABASE: staging
PGHOST: atat-db.postgres.database.azure.com
PGPORT: 5432
PGSSLMODE: verify-full
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
PGUSER: atat_master@atat-db
REDIS_HOST: atat.redis.cache.windows.net:6380
REDIS_TLS: "true"
SERVER_NAME: azure.atat.code.mil
TZ: UTC

View File

@ -34,9 +34,6 @@ spec:
- configMapRef:
name: atst-envvars
volumeMounts:
- name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini"
subPath: atst-overrides.ini
- name: nginx-client-ca-bundle
mountPath: "/opt/atat/atst/ssl/server-certs/ca-chain.pem"
subPath: client-ca-bundle.pem
@ -50,6 +47,8 @@ spec:
- name: uwsgi-config
mountPath: "/opt/atat/atst/uwsgi.ini"
subPath: uwsgi.ini
- name: flask-secret
mountPath: "/config"
- name: nginx
image: nginx:alpine
ports:
@ -79,13 +78,6 @@ spec:
- name: nginx-secret
mountPath: "/etc/ssl/"
volumes:
- name: atst-config
secret:
secretName: atst-config-ini
items:
- key: override.ini
path: atst-overrides.ini
mode: 0644
- name: nginx-client-ca-bundle
configMap:
name: nginx-client-ca-bundle
@ -141,6 +133,16 @@ spec:
keyvaultobjectaliases: "dhparam.pem;atat.key;atat.crt"
keyvaultobjecttypes: "secret;secret;secret"
tenantid: $TENANT_ID
- name: flask-secret
flexVolume:
driver: "azure/kv"
options:
usepodidentity: "true"
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
keyvaultobjecttypes: "secret;secret;secret;secret;key"
tenantid: $TENANT_ID
---
apiVersion: extensions/v1beta1
kind: Deployment
@ -161,6 +163,7 @@ spec:
labels:
app: atst
role: worker
aadpodidbinding: atat-kv-id-binding
spec:
securityContext:
fsGroup: 101
@ -182,20 +185,12 @@ spec:
- configMapRef:
name: atst-worker-envvars
volumeMounts:
- name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini"
subPath: atst-overrides.ini
- name: pgsslrootcert
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
subPath: pgsslrootcert.crt
- name: flask-secret
mountPath: "/config"
volumes:
- name: atst-config
secret:
secretName: atst-config-ini
items:
- key: override.ini
path: atst-overrides.ini
mode: 0644
- name: pgsslrootcert
configMap:
name: pgsslrootcert
@ -203,6 +198,16 @@ spec:
- key: cert
path: pgsslrootcert.crt
mode: 0666
- name: flask-secret
flexVolume:
driver: "azure/kv"
options:
usepodidentity: "true"
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
keyvaultobjecttypes: "secret;secret;secret;secret;key"
tenantid: $TENANT_ID
---
apiVersion: extensions/v1beta1
kind: Deployment
@ -223,6 +228,7 @@ spec:
labels:
app: atst
role: beat
aadpodidbinding: atat-kv-id-binding
spec:
securityContext:
fsGroup: 101
@ -244,20 +250,12 @@ spec:
- configMapRef:
name: atst-worker-envvars
volumeMounts:
- name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini"
subPath: atst-overrides.ini
- name: pgsslrootcert
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
subPath: pgsslrootcert.crt
- name: flask-secret
mountPath: "/config"
volumes:
- name: atst-config
secret:
secretName: atst-config-ini
items:
- key: override.ini
path: atst-overrides.ini
mode: 0644
- name: pgsslrootcert
configMap:
name: pgsslrootcert
@ -265,6 +263,16 @@ spec:
- key: cert
path: pgsslrootcert.crt
mode: 0666
- name: flask-secret
flexVolume:
driver: "azure/kv"
options:
usepodidentity: "true"
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
keyvaultobjecttypes: "secret;secret;secret;secret;key"
tenantid: $TENANT_ID
---
apiVersion: v1
kind: Service

View File

@ -10,6 +10,11 @@ spec:
jobTemplate:
spec:
template:
metadata:
labels:
app: atst
role: crl-sync
aadpodidbinding: atat-kv-id-binding
spec:
restartPolicy: OnFailure
containers:
@ -27,19 +32,21 @@ spec:
- configMapRef:
name: atst-worker-envvars
volumeMounts:
- name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini"
subPath: atst-overrides.ini
- name: crls-vol
mountPath: "/opt/atat/atst/crls"
- name: flask-secret
mountPath: "/config"
volumes:
- name: atst-config
secret:
secretName: atst-config-ini
items:
- key: override.ini
path: atst-overrides.ini
mode: 0644
- name: crls-vol
persistentVolumeClaim:
claimName: crls-vol-claim
- name: flask-secret
flexVolume:
driver: "azure/kv"
options:
usepodidentity: "true"
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
keyvaultobjecttypes: "secret;secret;secret;secret;key"
tenantid: $TENANT_ID

View File

@ -11,3 +11,52 @@ spec:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "dhparam4096;staging-cert;staging-cert"
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: atst-worker
spec:
template:
spec:
volumes:
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: atst-beat
spec:
template:
spec:
volumes:
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: crls
spec:
jobTemplate:
spec:
template:
spec:
volumes:
- name: flask-secret
flexVolume:
options:
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"

View File

@ -7,6 +7,11 @@ spec:
ttlSecondsAfterFinished: 100
backoffLimit: 2
template:
metadata:
labels:
app: atst
role: migration
aadpodidbinding: atat-kv-id-binding
spec:
containers:
- name: migration
@ -28,20 +33,12 @@ spec:
- configMapRef:
name: atst-worker-envvars
volumeMounts:
- name: atst-config
mountPath: "/opt/atat/atst/atst-overrides.ini"
subPath: atst-overrides.ini
- name: pgsslrootcert
mountPath: "/opt/atat/atst/ssl/pgsslrootcert.crt"
subPath: pgsslrootcert.crt
- name: flask-secret
mountPath: "/config"
volumes:
- name: atst-config
secret:
secretName: atst-config-ini
items:
- key: override.ini
path: atst-overrides.ini
mode: 0644
- name: pgsslrootcert
configMap:
name: pgsslrootcert
@ -49,4 +46,14 @@ spec:
- key: cert
path: pgsslrootcert.crt
mode: 0666
- name: flask-secret
flexVolume:
driver: "azure/kv"
options:
usepodidentity: "true"
keyvaultname: "atat-vault-test"
keyvaultobjectnames: "master-AZURE-STORAGE-KEY;master-MAIL-PASSWORD;master-PGPASSWORD;master-REDIS-PASSWORD;master-SECRET-KEY"
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
keyvaultobjecttypes: "secret;secret;secret;secret;key"
tenantid: $TENANT_ID
restartPolicy: Never

View File

@ -1,8 +1,13 @@
import os
from configparser import ConfigParser
import pytest
from atst.app import make_crl_validator
from atst.app import (
make_crl_validator,
apply_config_from_directory,
apply_config_from_environment,
)
@pytest.fixture
@ -22,3 +27,43 @@ def test_make_crl_validator_creates_crl_dir(app, tmpdir, replace_crl_dir_config)
replace_crl_dir_config(crl_dir)
make_crl_validator(app)
assert os.path.isdir(crl_dir)
@pytest.fixture
def config_object():
config = ConfigParser()
config.optionxform = str
config.read_string("[default]\nFOO=BALONEY")
return config
def test_apply_config_from_directory(tmpdir, config_object):
config_setting = tmpdir.join("FOO")
with open(config_setting, "w") as conf_file:
conf_file.write("MAYO")
apply_config_from_directory(tmpdir, config_object)
assert config_object.get("default", "FOO") == "MAYO"
def test_apply_config_from_directory_skips_unknown_settings(tmpdir, config_object):
config_setting = tmpdir.join("FLARF")
with open(config_setting, "w") as conf_file:
conf_file.write("MAYO")
apply_config_from_directory(tmpdir, config_object)
assert "FLARF" not in config_object.options("default")
def test_apply_config_from_environment(monkeypatch, config_object):
monkeypatch.setenv("FOO", "MAYO")
apply_config_from_environment(config_object)
assert config_object.get("default", "FOO") == "MAYO"
def test_apply_config_from_environment_skips_unknown_settings(
monkeypatch, config_object
):
monkeypatch.setenv("FLARF", "MAYO")
apply_config_from_environment(config_object)
assert "FLARF" not in config_object.options("default")