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

allow users to bring their own credentials and override MLZ Service Principal creation #315

Merged
merged 15 commits into from
Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from 12 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
65 changes: 52 additions & 13 deletions src/docs/command-line-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,19 +88,20 @@ If you don't wish to use those defaults, you can customize this command to targe

```plaintext
deploy.sh: create all the configuration and deploy Terraform resources with minimal input
argument description
--subscription-id -s Subscription ID for MissionLZ resources
--location -l [OPTIONAL] The location that you're deploying to (defaults to 'eastus')
--tf-environment -e [OPTIONAL] Terraform azurerm environment (defaults to 'public') see: https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment
--mlz-env-name -z [OPTIONAL] Unique name for MLZ environment (defaults to 'mlz' + UNIX timestamp)
--hub-sub-id -u [OPTIONAL] subscription ID for the hub network and resources (defaults to the value provided for -s --subscription-id)
--tier0-sub-id -0 [OPTIONAL] subscription ID for tier 0 network and resources (defaults to the value provided for -s --subscription-id)
--tier1-sub-id -1 [OPTIONAL] subscription ID for tier 1 network and resources (defaults to the value provided for -s --subscription-id)
--tier2-sub-id -2 [OPTIONAL] subscription ID for tier 2 network and resources (defaults to the value provided for -s --subscription-id)
--tier3-sub-id -3 [OPTIONAL] subscription ID for tier 3 network and resources (defaults to the value provided for -s --subscription-id), input is used in conjunction with deploy_t3.sh
--write-output -w [OPTIONAL] Tier 3 Deployment requires Terraform output, use this flag to write terraform output
--no-bastion [OPTIONAL] when present, do not create a Bastion Host and Jumpbox VM
--no-sentinel [OPTIONAL] when present, do not create an Azure Sentinel solution
argument description
--subscription-id -s Subscription ID for MissionLZ resources
--location -l [OPTIONAL] The location that you're deploying to (defaults to 'eastus')
--tf-environment -e [OPTIONAL] Terraform azurerm environment (defaults to 'public') see: https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment
--mlz-env-name -z [OPTIONAL] Unique name for MLZ environment (defaults to 'mlz' + UNIX timestamp)
--hub-sub-id -u [OPTIONAL] subscription ID for the hub network and resources (defaults to the value provided for -s --subscription-id)
--tier0-sub-id -0 [OPTIONAL] subscription ID for tier 0 network and resources (defaults to the value provided for -s --subscription-id)
--tier1-sub-id -1 [OPTIONAL] subscription ID for tier 1 network and resources (defaults to the value provided for -s --subscription-id)
--tier2-sub-id -2 [OPTIONAL] subscription ID for tier 2 network and resources (defaults to the value provided for -s --subscription-id)
--tier3-sub-id -3 [OPTIONAL] subscription ID for tier 3 network and resources (defaults to the value provided for -s --subscription-id), input is used in conjunction with deploy_t3.sh
--write-output -w [OPTIONAL] Tier 3 Deployment requires Terraform output, use this flag to write terraform output
--no-bastion [OPTIONAL] when present, do not create a Bastion Host and Jumpbox VM
--no-sentinel [OPTIONAL] when present, do not create an Azure Sentinel solution
--no-service-principal [OPTIONAL] when present, do not create an Azure Service Principal, instead use the credentials in the environment variables '$ARM_CLIENT_ID' and '$ARM_CLIENT_SECRET'
--help -h Print this message
```

Expand All @@ -117,6 +118,44 @@ src/scripts/deploy.sh -s {my_mlz_configuration_subscription_id} \

Need further customization? The rest of this documentation covers in detail how to customize this deployment to your needs.

#### Using your own Service Principal

Were you provided a subscription(s) and credentials to use, or do you already have an identity you want to use to deploy and manage Terraform with?

By default, Mission LZ will attempt to create a Service Principal to deploy and manage Terraform on your behalf.

> **NOTE:** If you are providing your own Service Principal, that Service Principal must have at minimum a 'Contributor' role.

To use your own Service Principal credentials, first, set ARM_CLIENT_ID and ARM_CLIENT_SECRET environment variables:

```bash
export ARM_CLIENT_ID="{YOUR_SERVICE_PRINCIPAL_CLIENT_ID}"
export ARM_CLIENT_SECRET="{YOUR_SERVICE_PRINCIPAL_CLIENT_SECRET}"
```

Then, specify the `--no-service-principal` flag when running `deploy.sh`:

```bash
deploy.sh --subscription-id "{YOUR_SUBSCRIPTION_ID}" --no-service-principal
```

If you use `--no-service-principal` without `ARM_CLIENT_ID` and `ARM_CLIENT_SECRET` set in your environment, you will recieve an error:

```plaintext
ERROR: When specifying --no-service-principal, these environment variables are mandatory: ARM_CLIENT_ID, ARM_CLIENT_SECRET
INFO: You can set these environment variables with 'export ARM_CLIENT_ID="YOUR_CLIENT_ID"' and 'export ARM_CLIENT_SECRET="YOUR_CLIENT_SECRET"'
```

If you use `--no-service-principal` but the Service Principal you supply with `ARM_CLIENT_ID` does not have "Contributor" RBAC permissions for the subscriptions you wish to deploy into, you will receive an error:

```plaintext:
ERROR: service principal with client ID AAAAAAAA-BBBB-CCCC-DDDDDDDDDD does not have 'Contributor' or 'Owner' roles for subscription 00000000-1111-2222-333333333333!
INFO: at minimum, the 'Contributor' role is required to manage resources via Terraform.
INFO: to set this role for this subscription, a user with the 'Owner' role can try this command:
INFO: az role assignment create --assignee-object-id EEEEEEEE-FFFF-GGGG-HHHHHHHHHH --role "Contributor" --scope "/subscriptions/00000000-1111-2222-333333333333"
ERROR: please assign the 'Contributor' role to this subscription and try again.
```

## Setup Mission LZ Resources

Deployment of MLZ happens through use of a single Service Principal whose credentials are stored in a central "config" Key Vault.
Expand Down
9 changes: 7 additions & 2 deletions src/scripts/config/config_clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,13 @@ do
done
done

echo "INFO: deleting service principal ${mlz_sp_name}..."
az ad sp delete --id $(az ad sp list --display-name "http://${mlz_sp_name}" --query [0].appId --output tsv)
echo "INFO: querying for any created service principal with name ${mlz_sp_name}..."
sp_id=$(az ad sp list --display-name "http://${mlz_sp_name}" --query [0].appId --output tsv)

if [[ $sp_id ]]; then
echo "INFO: deleting service principal ${mlz_sp_name}..."
az ad sp delete --id "${sp_id}"
fi

echo "INFO: purging key vault ${mlz_kv_name}..."
az keyvault purge \
Expand Down
96 changes: 63 additions & 33 deletions src/scripts/config/create_mlz_config_resources.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ error_log() {

usage() {
echo "create_mlz_config_resources.sh: Create MLZ config resources"
error_log "usage: create_mlz_config_resources.sh <mlz config>"
error_log "usage: create_mlz_config_resources.sh <mlz config> <create service principal (true or false)>"
}

if [[ "$#" -lt 1 ]]; then
Expand All @@ -26,6 +26,9 @@ if [[ "$#" -lt 1 ]]; then
fi

mlz_config=$(realpath "${1}")
create_service_principal=${2:-true}

this_script_path=$(realpath "${BASH_SOURCE%/*}")

# Source variables
. "${mlz_config}"
Expand Down Expand Up @@ -112,43 +115,70 @@ wait_for_sp_property() {
done
}

# Create Azure AD application registration and Service Principal
# TODO: Lift the subscription scoping out of here and move into conditional
echo "INFO: verifying service principal ${mlz_sp_name} is unique..."
if [[ -z $(az ad sp list --filter "displayName eq 'http://${mlz_sp_name}'" --query "[].displayName" -o tsv) ]];then
echo "INFO: creating service principal ${mlz_sp_name}..."
sp_creds=($(az ad sp create-for-rbac \
--name "http://${mlz_sp_name}" \
--skip-assignment true \
--query "[password, appId]" \
--only-show-errors \
--output tsv))

sp_client_secret=${sp_creds[0]}
sp_client_id=${sp_creds[1]}
check_for_arm_credential() {
util_path=$(realpath "${this_script_path}/../util")
"${util_path}/checkforarmcredential.sh" "ERROR: When using a user-provided service principal, these environment variables are mandatory: ARM_CLIENT_ID, ARM_CLIENT_SECRET"
}

wait_for_sp_creation "${sp_client_id}"
wait_for_sp_property "${sp_client_id}" "objectId"
validate_minimum_role_for_sp() {
"${this_script_path}/validate_minimum_role_for_sp.sh" "${mlz_config}" "${ARM_CLIENT_ID}"
}

odata_filter_args=(--filter "\"appId eq '$sp_client_id'\"" --query "[0].objectId" --output tsv)
object_id_query="az ad sp list ${odata_filter_args[*]}"
# Create Service Principal
if [[ "${create_service_principal}" == false ]];
then
check_for_arm_credential
validate_minimum_role_for_sp

sp_object_id=$(eval "$object_id_query")
echo "INFO: using user-supplied service principal with client ID ${ARM_CLIENT_ID}..."

# Assign Contributor role to Service Principal
for sub in "${subs[@]}"
do
echo "INFO: setting Contributor role assignment for ${mlz_sp_name} on subscription ${sub}..."
az role assignment create \
--role Contributor \
--assignee-object-id "${sp_object_id}" \
--scope "/subscriptions/${sub}" \
--assignee-principal-type ServicePrincipal \
--output none
done
sp_client_id="${ARM_CLIENT_ID}"
sp_client_secret="${ARM_CLIENT_SECRET}"
sp_object_id=$(az ad sp list \
--filter "appId eq '${ARM_CLIENT_ID}'" \
--query "[].objectId" \
--output tsv)
else
error_log "ERROR: A service principal named ${mlz_sp_name} already exists. This must be a unique service principal for your use only. Try again with a new mlz-env-name. Exiting script."
exit 1
echo "INFO: verifying service principal ${mlz_sp_name} is unique..."
if [[ -z $(az ad sp list \
--filter "displayName eq 'http://${mlz_sp_name}'" \
--query "[].displayName" \
--output tsv) ]];
then
echo "INFO: creating service principal ${mlz_sp_name}..."
sp_creds=($(az ad sp create-for-rbac \
--name "http://${mlz_sp_name}" \
--skip-assignment true \
--query "[password, appId]" \
--only-show-errors \
--output tsv))

sp_client_secret=${sp_creds[0]}
sp_client_id=${sp_creds[1]}

wait_for_sp_creation "${sp_client_id}"
wait_for_sp_property "${sp_client_id}" "objectId"

odata_filter_args=(--filter "\"appId eq '$sp_client_id'\"" --query "[0].objectId" --output tsv)
object_id_query="az ad sp list ${odata_filter_args[*]}"

sp_object_id=$(eval "$object_id_query")

# Assign Contributor Role to Subscriptions
for sub in "${subs[@]}"
do
echo "INFO: setting Contributor role assignment for ${sp_client_id} on subscription ${sub}..."
az role assignment create \
--role Contributor \
--assignee-object-id "${sp_object_id}" \
--scope "/subscriptions/${sub}" \
--assignee-principal-type ServicePrincipal \
--output none
done
else
error_log "ERROR: A service principal named ${mlz_sp_name} already exists. This must be a unique service principal for your use only. Try again with a new mlz-env-name. Exiting script."
exit 1
fi
fi

# Validate or create Terraform Config resource group
Expand Down
7 changes: 5 additions & 2 deletions src/scripts/config/create_required_resources.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ error_log() {

usage() {
echo "create_required_resources.sh: configure a resource group that contains Terraform state and a secret store"
error_log "usage: create_required_resources.sh <mlz config>"
error_log "usage: create_required_resources.sh <mlz config> <create service principal (true or false)>"
}

if [[ "$#" -lt 1 ]]; then
Expand All @@ -27,6 +27,7 @@ if [[ "$#" -lt 1 ]]; then
fi

mlz_config=$(realpath "${1}")
create_service_principal=${2:-true}

this_script_path=$(realpath "${BASH_SOURCE%/*}")

Expand All @@ -39,7 +40,9 @@ mlz_path="$(realpath "${this_script_path}/../../terraform/mlz")"
. "${mlz_config}"

# create MLZ configuration resources
. "${this_script_path}/create_mlz_config_resources.sh" "${mlz_config}" "${mlz_env_name}" "${mlz_config_location}"
. "${this_script_path}/create_mlz_config_resources.sh" \
"${mlz_config}" \
"${create_service_principal}"

# create terraform resources given a subscription ID and terraform configuration folder
. "${this_script_path}/create_terraform_backend_resources.sh" "${mlz_config}" "${mlz_config_subid}" "${mlz_path}"
78 changes: 78 additions & 0 deletions src/scripts/config/validate_minimum_role_for_sp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/bash
#
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
#
# validates that a Service Principal has 'Contributor' or 'Owner'
# role assigned for the subscriptions in a given .mlzconfig

set -e

error_log() {
echo "${1}" 1>&2;
}

usage() {
error_log "usage: validate_minimum_role_for_sp.sh <mlz config> <client ID>"
echo "validate_minimum_role_for_sp.sh: validates that a Service Principal for a given Client ID has 'Contributor' or 'Owner' role assigned for the subscriptions in a given .mlzconfig"
}

if [[ "$#" -lt 2 ]]; then
usage
exit 1
fi

mlz_config=$(realpath "${1}")
client_id=${2}

# Source variables
. "${mlz_config}"
glennmusa marked this conversation as resolved.
Show resolved Hide resolved

# Create array of unique subscription IDs. The 'sed' command below search thru the source
# variables file looking for all lines that do not have a '#' in the line. If a line with
# a '#' is found, the '#' and ever character after it in the line is ignored. The output
# of what remains from the sed command is then piped to grep to find the words that match
# the pattern. These words are what make up the 'mlz_subs' array.
mlz_sub_pattern="mlz_.*._subid"
mlz_subs=$(< "${mlz_config}" sed 's:#.*$::g' | grep -w "${mlz_sub_pattern}")
subs=()
for mlz_sub in $mlz_subs
do
mlz_sub_id=$(echo "${mlz_sub#*=}" | tr -d '"')
if [[ ! "${subs[*]}" =~ ${mlz_sub_id} ]];then
subs+=("${mlz_sub_id}")
fi
done

object_id=$(az ad sp list \
--filter "appId eq '${client_id}'" \
--query "[].objectId" \
--output tsv)

subs_requiring_role_assignment=()

for sub in ${subs[@]}
glennmusa marked this conversation as resolved.
Show resolved Hide resolved
do
valid_assignments=$(az role assignment list \
--assignee "${object_id}" \
--scope "/subscriptions/${sub}" \
--query "[?roleDefinitionName=='Contributor' || roleDefinitionName=='Owner'].{scope: scope}" \
--output tsv)
if [[ -z $valid_assignments ]]; then
subs_requiring_role_assignment+=("${sub}")
fi
done

if [[ ${#subs_requiring_role_assignment[@]} -gt 0 ]]; then
error_log "ERROR: service principal with client ID ${client_id} is missing 'Contributor' role!"
echo "INFO: at minimum, the 'Contributor' role is required to manage resources via Terraform."
echo "INFO: to set this role for the relevant subscriptions, a user with the 'Owner' role can try these commands:"

for sub in ${subs_requiring_role_assignment[@]}
glennmusa marked this conversation as resolved.
Show resolved Hide resolved
do
echo "INFO: az role assignment create --assignee-object-id ${object_id} --role \"Contributor\" --scope \"/subscriptions/${sub}\""
done

error_log "ERROR: please assign the 'Contributor' role to this service principal and try again."
exit 1
fi
16 changes: 14 additions & 2 deletions src/scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ show_help() {
local long_name=$1
local char_name=$2
local desc=$3
printf "%20s %2s %s \n" "$long_name" "$char_name" "$desc"
printf "%25s %2s %s \n" "$long_name" "$char_name" "$desc"
}
print_formatted "--------" "" "-----------"
print_formatted "argument" "" "description"
print_formatted "--------" "" "-----------"
print_formatted "--subscription-id" "-s" "Subscription ID for MissionLZ resources"
print_formatted "--location" "-l" "[OPTIONAL] The location that you're deploying to (defaults to 'eastus')"
print_formatted "--tf-environment" "-e" "[OPTIONAL] Terraform azurerm environment (defaults to 'public') see: https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment"
Expand All @@ -33,6 +35,7 @@ show_help() {
print_formatted "--write-output" "-w" "[OPTIONAL] Tier 3 Deployment requires Terraform output, use this flag to write terraform output"
print_formatted "--no-bastion" "" "[OPTIONAL] when present, do not create a Bastion Host and Jumpbox VM"
print_formatted "--no-sentinel" "" "[OPTIONAL] when present, do not create an Azure Sentinel solution"
print_formatted "--no-service-principal" "" "[OPTIONAL] when present, do not create an Azure Service Principal, instead use the credentials in the environment variables '\$ARM_CLIENT_ID' and '\$ARM_CLIENT_SECRET'"
print_formatted "--help" "-h" "Print this message"
}

Expand Down Expand Up @@ -70,6 +73,12 @@ inspect_user_input() {
log_default "--location" "${default_config_location}" "${mlz_config_location}"
log_default "--tf-environment" "${default_tf_environment}" "${tf_environment}"
log_default "--mlz-env-name" "${default_env_name}" "${mlz_env_name}"

# if the user has set --no-service-principal, ensure mandatory environment variables are set
# and that the service principal exists
if [[ "${create_service_principal}" == false ]]; then
"${this_script_path}/util/checkforarmcredential.sh" "ERROR: When specifying --no-service-principal, these environment variables are mandatory: ARM_CLIENT_ID, ARM_CLIENT_SECRET"
fi
}

login_azcli() {
Expand Down Expand Up @@ -141,7 +150,7 @@ validate_mlz_configuration_file() {

create_mlz_resources() {
echo "INFO: creating MLZ resources using ${mlz_config_file_path}..."
"${this_script_path}/config/create_required_resources.sh" "${mlz_config_file_path}"
"${this_script_path}/config/create_required_resources.sh" "${mlz_config_file_path}" "${create_service_principal}"
}

create_terraform_variables() {
Expand Down Expand Up @@ -184,6 +193,7 @@ default_tf_environment="public"
default_env_name="mlz${timestamp}"
create_bastion_jumpbox=true
create_sentinel=true
create_service_principal=true

mlz_config_subid="${default_config_subid}"
mlz_config_location="${default_config_location}"
Expand Down Expand Up @@ -227,6 +237,8 @@ while [ $# -gt 0 ] ; do
create_bastion_jumpbox=false ;;
--no-sentinel)
create_sentinel=false ;;
--no-service-principal)
create_service_principal=false ;;
-h | --help)
show_help
exit 0 ;;
Expand Down
Loading