Merge branch 'staging' into disable-pod-escalation

This commit is contained in:
dandds 2020-01-28 16:48:45 -05:00 committed by GitHub
commit 81a41a632a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 358 additions and 34 deletions

View File

@ -21,11 +21,8 @@ LICENSE
# Skip envrc # Skip envrc
.envrc .envrc
# Skip ansible-container stuff # Skip terraform
ansible* terraform
container.yml
meta.yml
requirements.yml
# Skip kubernetes and Docker config stuff # Skip kubernetes and Docker config stuff
deploy deploy

View File

@ -93,10 +93,13 @@ class Users(object):
return user return user
@classmethod @classmethod
def give_ccpo_perms(cls, user): def give_ccpo_perms(cls, user, commit=True):
user.permission_sets = PermissionSets.get_all() user.permission_sets = PermissionSets.get_all()
db.session.add(user) db.session.add(user)
db.session.commit()
if commit:
db.session.commit()
return user return user
@classmethod @classmethod

41
script/create_database.py Normal file
View File

@ -0,0 +1,41 @@
# Add root application dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(parent_dir)
import sqlalchemy
from atst.app import make_config
def _root_connection(config, root_db):
# Assemble DATABASE_URI value
database_uri = "postgresql://{}:{}@{}:{}/{}".format( # pragma: allowlist secret
config.get("PGUSER"),
config.get("PGPASSWORD"),
config.get("PGHOST"),
config.get("PGPORT"),
root_db,
)
engine = sqlalchemy.create_engine(database_uri)
return engine.connect()
def create_database(conn, dbname):
conn.execute("commit")
conn.execute(f"CREATE DATABASE {dbname};")
conn.close()
return True
if __name__ == "__main__":
dbname = sys.argv[1]
config = make_config()
conn = _root_connection(config, "postgres")
print(f"Creating database {dbname}")
create_database(conn, dbname)

76
script/database_setup.py Normal file
View File

@ -0,0 +1,76 @@
# Add root application dir to the python path
import os
import sys
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(parent_dir)
import sqlalchemy
import yaml
from atst.app import make_config, make_app
from atst.database import db
from atst.domain.users import Users
from atst.models import User
from reset_database import reset_database
def database_setup(username, password, dbname, ccpo_users):
print(
f"Creating Postgres user role for '{username}' and granting all privileges to database '{dbname}'."
)
try:
_create_database_user(username, password, dbname)
except sqlalchemy.exc.ProgrammingError as err:
print(f"Postgres user role '{username}' already exists.")
print("Applying schema and seeding roles and permissions.")
reset_database()
print("Creating initial set of CCPO users.")
_add_ccpo_users(ccpo_users)
def _create_database_user(username, password, dbname):
conn = db.engine.connect()
meta = sqlalchemy.MetaData(bind=conn)
meta.reflect()
trans = conn.begin()
engine = trans.connection.engine
engine.execute(
f"CREATE ROLE {username} WITH LOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION PASSWORD '{password}';\n"
f"GRANT ALL PRIVILEGES ON DATABASE {dbname} TO {username};\n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {username}; \n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO {username}; \n"
f"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO {username}; \n"
)
trans.commit()
def _add_ccpo_users(ccpo_users):
for user_data in ccpo_users:
user = User(**user_data)
Users.give_ccpo_perms(user, commit=False)
db.session.add(user)
db.session.commit()
def _load_yaml(file_):
with open(file_) as f:
return yaml.safe_load(f)
if __name__ == "__main__":
config = make_config({"DISABLE_CRL_CHECK": True, "DEBUG": False})
app = make_app(config)
with app.app_context():
dbname = config.get("PGDATABASE", "atat")
username = sys.argv[1]
password = sys.argv[2]
ccpo_user_file = sys.argv[3]
ccpo_users = _load_yaml(ccpo_user_file)
database_setup(username, password, dbname, ccpo_users)

View File

@ -16,7 +16,9 @@ from atst.app import make_config, make_app
def reset_database(): def reset_database():
conn = db.engine.connect() conn = db.engine.connect()
meta = sqlalchemy.MetaData(bind=conn, reflect=True) meta = sqlalchemy.MetaData(bind=conn)
meta.reflect()
trans = conn.begin() trans = conn.begin()
# drop all tables # drop all tables

View File

@ -1,11 +1,11 @@
# ATAT Terraform # ATAT Terraform
Welcome! You've found the ATAT IaC configurations. Welcome! You've found the ATAT IaC configurations.
ATAT is configured using terraform and a wrapper script called `secrets-tool`. With `terraform` we can configure infrastructure in a programatic way and ensure consistency across environments. ATAT is configured using terraform and a wrapper script called `secrets-tool`. With `terraform` we can configure infrastructure in a programatic way and ensure consistency across environments.
## Directory Structure ## Directory Structure
**modules/** - Terraform modules. These are modules that can be re-used for multiple environments. **modules/** - Terraform modules. These are modules that can be re-used for multiple environments.
**providers/** - Specific environment configurations. (dev,production, etc) **providers/** - Specific environment configurations. (dev,production, etc)
@ -92,7 +92,7 @@ Check the output for errors. Sometimes the syntax is valid, but some of the conf
# After running TF (Manual Steps) # After running TF (Manual Steps)
## VM Scale Set ## VM Scale Set
After running terraform, we need to make a manual change to the VM Scale Set that is used in the kubernetes. Terraform has a bug that is not applying this as of `v1.40` of the `azurerm` provider. After running terraform, we need to make a manual change to the VM Scale Set that is used in the kubernetes. Terraform has a bug that is not applying this as of `v1.40` of the `azurerm` provider.
In order to get the `SystemAssigned` identity to be set, it needs to be set manually in the console. In order to get the `SystemAssigned` identity to be set, it needs to be set manually in the console.
@ -253,7 +253,7 @@ Uncomment the `backend {}` section in the `provider.tf` file. Once uncommented,
*Say `yes` to the question* *Say `yes` to the question*
Now we need to update the Update `variables.tf` with the principals for the users in `admin_users` variable map. If these are not defined yet, just leave it as an empty set. Now we need to update the Update `variables.tf` with the principals for the users in `admin_users` variable map. If these are not defined yet, just leave it as an empty set.
Next, we'll create the operator keyvault. Next, we'll create the operator keyvault.
@ -281,4 +281,25 @@ secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ cr
`terraform apply` `terraform apply`
*[Configure AD for MFA](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-mfa)* *[Configure AD for MFA](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-mfa)*
*Then we need an instance of the container*
Change directories to the repo root. Ensure that you've checked out the staging or master branch:
`docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:latest`
*Create secrets for ATAT database user*
Change directories back to terraform/secrets-tool. There is a sample file there. Make sure you know the URL for the aplication Key Vault (distinct from the operator Key Vault). Run:
`secrets-tool secrets --keyvault [application key vault URL] load -f ./postgres-user.yaml
*Create the database, database user, schema, and initial data set*
This is discussed in more detail [here](https://github.com/dod-ccpo/atst/tree/staging/terraform/secrets-tool#setting-up-the-initial-atat-database). Be sure to read the requirements section.
```
secrets-tool database --keyvault [operator key vault URL] provision --app-keyvault [application key vault URL] --dbname jedidev-atat --dbhost [database host name] --ccpo-users /full/path/to/users.yml
```

View File

@ -35,11 +35,3 @@ resource "azurerm_postgresql_virtual_network_rule" "sql" {
subnet_id = var.subnet_id subnet_id = var.subnet_id
ignore_missing_vnet_service_endpoint = true ignore_missing_vnet_service_endpoint = true
} }
resource "azurerm_postgresql_database" "db" {
name = "${var.name}-${var.environment}-atat"
resource_group_name = azurerm_resource_group.sql.name
server_name = azurerm_postgresql_server.sql.name
charset = "UTF8"
collation = "en-US"
}

View File

@ -1,3 +0,0 @@
output "db_name" {
value = azurerm_postgresql_database.db.name
}

View File

@ -15,7 +15,7 @@ With both usernames and passwords generated, the application only needs to make
Ex. Ex.
``` ```
{ {
'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl', 'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl',
'postgres_root_password': "2+[A@E4:C=ubb/#R#'n<p|wCW-|%q^" 'postgres_root_password': "2+[A@E4:C=ubb/#R#'n<p|wCW-|%q^"
} }
``` ```
@ -30,6 +30,51 @@ Terraform typically expects user defined secrets to be stored in either a file,
This provides a number of security benefits. First, secrets are not on disk. Secondly, users/operators never see the secrets fly by (passerbys or voyeurs that like to look over your shoulder when deploying to production) This provides a number of security benefits. First, secrets are not on disk. Secondly, users/operators never see the secrets fly by (passerbys or voyeurs that like to look over your shoulder when deploying to production)
## Setting up the initial ATAT database
This handles bootstrapping the ATAT database with a user, schema, and initial data.
It does the following:
- Sources the Postgres root user credentials
- Source the Postgres ATAT user password
- Runs a script inside an ATAT docker container to set up the initial database user, schema, and seed data in the database
Requirements:
- docker
- A copy of the ATAT docker image. This can be built in the repo root with: `docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:latest`
- You need to know the hostname for the Postgres database. Your IP must either be whitelisted in its firewall rules or you must be behind the VPN.
- You will need a YAML file listing all the CCPO users to be added to the database, with the format:
```
- dod_id: "2323232323"
first_name: "Luke"
last_name: "Skywalker"
- dod_id: "5656565656"
first_name: "Han"
last_name: "Solo"
```
- There should be a password for the ATAT database user in the application Key Vault, preferably named `PGPASSWORD`. You can load this by running `secrets-tool --keyvault [operator key vault url] load -f postgres-user.yml` and supplying YAML like:
```
---
- PGPASSWORD:
type: 'password'
length: 30
```
This command takes a lot of arguments. Run `secrets-tool database --keyvault [operator key vault url] provision -- help` to see the full list of available options.
The command supplies some defaults by assuming you've followed the patterns in sample-secrets.yml and elsewhere.
An example would be:
```
secrets-tool database --keyvault [operator key vault URL] provision --app-keyvault [application key vault URL] --dbname jedidev-atat --dbhost [database host name] --ccpo-users /full/path/to/users.yml
```
# Setup # Setup
*Requirements* *Requirements*
@ -76,4 +121,4 @@ secrets-tool secrets --keyvault https://operator-dev-keyvault.vault.azure.net/ l
This will fetch all secrets from the keyvault specified. `secrets-tool` then converts the keys to a variable name that terraform will look for. Essentially it prepends the keys found in KeyVault with `TF_VAR` and then executes terraform as a subprocess with the injected environment variables. This will fetch all secrets from the keyvault specified. `secrets-tool` then converts the keys to a variable name that terraform will look for. Essentially it prepends the keys found in KeyVault with `TF_VAR` and then executes terraform as a subprocess with the injected environment variables.
``` ```
secrets-tool terraform --keyvault https://operator-dev-keyvault.vault.azure.net/ plan secrets-tool terraform --keyvault https://operator-dev-keyvault.vault.azure.net/ plan
``` ```

View File

@ -0,0 +1,143 @@
import os
import click
import logging
import subprocess
from utils.keyvault.secrets import SecretsClient
logger = logging.getLogger(__name__)
def _run_cmd(command):
try:
env = os.environ.copy()
with subprocess.Popen(
command, env=env, stdout=subprocess.PIPE, shell=True
) as proc:
for line in proc.stdout:
logging.info(line.decode("utf-8"))
except Exception as e:
print(e)
@click.group()
@click.option("--keyvault", required=True, help="Specify the keyvault to operate on")
@click.pass_context
def database(ctx, keyvault):
ctx.ensure_object(dict)
ctx.obj["keyvault"] = keyvault
# root password, root username
@click.command("provision")
@click.option(
"--app-keyvault",
"app_keyvault",
required=True,
help="The username for the new Postgres user.",
)
@click.option(
"--user-username",
"user_username",
default="atat",
required=True,
help="The username for the new Postgres user.",
)
@click.option(
"--user-password-key",
"user_password_key",
default="PGPASSWORD",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--root-username-key",
"root_username_key",
default="postgres-root-user",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--root-password-key",
"root_password_key",
default="postgres-root-password",
required=True,
help="The name of the user's password key in the specified vault.",
)
@click.option(
"--dbname",
"dbname",
required=True,
help="The name of the database the user will be given full access to.",
)
@click.option(
"--dbhost",
"dbhost",
required=True,
help="The name of the database the user will be given full access to.",
)
@click.option(
"--container",
"container",
default="atat:latest",
required=True,
help="The container to run the provisioning command in.",
)
@click.option(
"--ccpo-users",
"ccpo_users",
required=True,
help="The full path to a YAML file listing CCPO users to be seeded to the database.",
)
@click.pass_context
def provision(
ctx,
app_keyvault,
user_username,
user_password_key,
root_username_key,
root_password_key,
dbname,
dbhost,
container,
ccpo_users,
):
"""
Set up the initial ATAT database.
"""
logger.info("obtaining postgres root user credentials")
operator_keyvault = SecretsClient(vault_url=ctx.obj["keyvault"])
root_password = operator_keyvault.get_secret(root_password_key)
root_name = operator_keyvault.get_secret(root_username_key)
logger.info("obtaining postgres database user password")
app_keyvault = SecretsClient(vault_url=app_keyvault)
user_password = app_keyvault.get_secret(user_password_key)
logger.info("starting docker process")
create_database_cmd = (
f"docker run -e PGHOST='{dbhost}'"
f" -e PGPASSWORD='{root_password}'"
f" -e PGUSER='{root_name}@{dbhost}'"
f" -e PGDATABASE='{dbname}'"
f" -e PGSSLMODE=require"
f" {container}"
f" .venv/bin/python script/create_database.py {dbname}"
)
_run_cmd(create_database_cmd)
seed_database_cmd = (
f"docker run -e PGHOST='{dbhost}'"
f" -e PGPASSWORD='{root_password}'"
f" -e PGUSER='{root_name}@{dbhost}'"
f" -e PGDATABASE='{dbname}'"
f" -e PGSSLMODE=require"
f" -v {ccpo_users}:/opt/atat/atst/users.yml"
f" {container}"
f" .venv/bin/python script/database_setup.py {user_username} '{user_password}' users.yml"
)
_run_cmd(seed_database_cmd)
database.add_command(provision)

View File

@ -0,0 +1,4 @@
---
- PGPASSWORD:
type: 'password'
length: 30

View File

@ -7,6 +7,7 @@ import logging
from commands.secrets import secrets from commands.secrets import secrets
from commands.terraform import terraform from commands.terraform import terraform
from commands.database import database
config.setup_logging() config.setup_logging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,6 +22,7 @@ def cli():
# Add additional command groups # Add additional command groups
cli.add_command(secrets) cli.add_command(secrets)
cli.add_command(terraform) cli.add_command(terraform)
cli.add_command(database)
if __name__ == "__main__": if __name__ == "__main__":
@ -41,12 +43,12 @@ if __name__ == "__main__":
val = keyvault.get_secret(secret) val = keyvault.get_secret(secret)
#print(val) #print(val)
os.environ[name] = val os.environ[name] = val
env = os.environ.copy() env = os.environ.copy()
command = "{} {}".format(PROCESS, sys.argv[1]) command = "{} {}".format(PROCESS, sys.argv[1])
with subprocess.Popen(command, env=env, stdout=subprocess.PIPE, shell=True) as proc: with subprocess.Popen(command, env=env, stdout=subprocess.PIPE, shell=True) as proc:
for line in proc.stdout: for line in proc.stdout:
logging.info(line.decode("utf-8") ) logging.info(line.decode("utf-8") )
except Exception as e: except Exception as e:
print(e, traceback.print_stack) print(e, traceback.print_stack)
''' '''

View File

@ -36,7 +36,7 @@ class SecretsLoader():
load the secrets in to keyvault load the secrets in to keyvault
""" """
def __init__(self, yaml_file: str, keyvault: object): def __init__(self, yaml_file: str, keyvault: object):
assert Path(yaml_file).exists() assert Path(yaml_file).exists()
self.yaml_file = yaml_file self.yaml_file = yaml_file
self.keyvault = keyvault self.keyvault = keyvault
self.config = dict() self.config = dict()
@ -47,7 +47,7 @@ class SecretsLoader():
def _load_yaml(self): def _load_yaml(self):
with open(self.yaml_file) as handle: with open(self.yaml_file) as handle:
self.config = yaml.load(handle, Loader=yaml.FullLoader) self.config = yaml.load(handle, Loader=yaml.FullLoader)
def _generate_secrets(self): def _generate_secrets(self):
secrets = GenerateSecrets(self.config).process_definition() secrets = GenerateSecrets(self.config).process_definition()
self.secrets = secrets self.secrets = secrets
@ -60,12 +60,14 @@ class SecretsLoader():
class GenerateSecrets(): class GenerateSecrets():
""" """
Read the secrets definition and generate requiesite Read the secrets definition and generate requiesite
secrets based on the type of secret and arguments secrets based on the type of secret and arguments
provided provided
""" """
def __init__(self, definitions: dict): def __init__(self, definitions: dict):
self.definitions = definitions self.definitions = definitions
most_punctuation = string.punctuation.replace("'", "").replace('"', "")
self.password_characters = string.ascii_letters + string.digits + most_punctuation
def process_definition(self): def process_definition(self):
""" """
@ -101,9 +103,8 @@ class GenerateSecrets():
# Types. Can be usernames, passwords, or in the future things like salted # Types. Can be usernames, passwords, or in the future things like salted
# tokens, uuid, or other specialized types # tokens, uuid, or other specialized types
def _generate_password(self, length: int): def _generate_password(self, length: int):
self.password_characters = string.ascii_letters + string.digits + string.punctuation
return ''.join(secrets.choice(self.password_characters) for i in range(length)) return ''.join(secrets.choice(self.password_characters) for i in range(length))
def _generate_username(self, length: int): def _generate_username(self, length: int):
self.username_characters = string.ascii_letters self.username_characters = string.ascii_letters
return ''.join(secrets.choice(self.username_characters) for i in range(length)) return ''.join(secrets.choice(self.username_characters) for i in range(length))