|
| 1 | +# MongoDB Atlas Provider -- OIDC Workload Azure |
| 2 | + |
| 3 | +## Dependencies |
| 4 | + |
| 5 | +- Terraform [MongoDB Atlas Provider v1.17.0](https://registry.terraform.io/providers/mongodb/mongodbatlas/latest/docs) |
| 6 | +- Terraform [Hashicorp Cloud Init Provider v2.3.4](https://registry.terraform.io/providers/hashicorp/cloudinit/latest/docs) |
| 7 | +- Terraform [Hashicorp Azurerm Provider v3.106.1](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) |
| 8 | +- Terraform [Hashicorp Random Provider v3.6.2](https://registry.terraform.io/providers/hashicorp/random/latest/docs) |
| 9 | +- A MongoDB Atlas account with an organization configured with [Federated Authentication](https://www.mongodb.com/docs/atlas/security/federated-authentication/#federation-management-console) |
| 10 | + - Get the `federated_settings_id` from the url, e.g., <https://cloud.mongodb.com/v2#/federation/{federated_settings_id}/overview> |
| 11 | + |
| 12 | +## Diagrams |
| 13 | + |
| 14 | +### Simplified OIDC flow |
| 15 | + |
| 16 | + |
| 17 | +```mermaid |
| 18 | +--- |
| 19 | +title: "Simplified OIDC flow" |
| 20 | +--- |
| 21 | +sequenceDiagram |
| 22 | + autonumber |
| 23 | + participant U as User/App |
| 24 | + participant A as Atlas |
| 25 | + participant Idp as Identity Provider (IdP) |
| 26 | +
|
| 27 | + Note right of A: OIDC is configured and set on the organization<br>issuer_uri=https://{some-idp}.com/ |
| 28 | + A->>Idp: issuer_uri/.well-known/openid-configuration |
| 29 | + Idp->>A: {..., "jwks_uri": "jwks_uri"} |
| 30 | + A->>Idp: jwks_uri |
| 31 | + Idp->>A: {keys: [{"kid": kid}, ...]} |
| 32 | +
|
| 33 | + Note right of U: Obtain token |
| 34 | + U->>Idp: get_token(authentication-mechanism) |
| 35 | + Idp->>U: JWT token {"iss": "issuer_uri", sub: "user-identity", "kid": "key-used, ...} |
| 36 | +
|
| 37 | + Note right of A: A database user is registered with idp_id/sub as username |
| 38 | + U->>A: connect(JWT token) |
| 39 | + A->>A: use kid and keys to validate the token<br>lookup sub with registered users |
| 40 | + A->>U: auth ok |
| 41 | +``` |
| 42 | + |
| 43 | +- Example of `issuer_uri` (1): |
| 44 | + - <https://token.actions.githubusercontent.com/> |
| 45 | + - <https://gitlab.com/> |
| 46 | + - <https://sts.windows.net/{tenant-id}/> |
| 47 | +- Example of authentication mechanism (5) |
| 48 | + - username+password |
| 49 | + - two factor authentication |
| 50 | + - certificate |
| 51 | +- JWT Token Authentication(8): |
| 52 | + - Can use different fields in the `JWT` to authenticate |
| 53 | + |
| 54 | +### OIDC Workload Azure |
| 55 | + |
| 56 | +```mermaid |
| 57 | +--- |
| 58 | +title: "OIDC Workload Azure" |
| 59 | +--- |
| 60 | +sequenceDiagram |
| 61 | + autonumber |
| 62 | + participant VM as VM<br>(Azure) |
| 63 | + participant A as Atlas |
| 64 | + participant Idp as IdP<br>(Azure) |
| 65 | + |
| 66 | + Note right of A: OIDC is configured and set on the organization<br>Atlas will keep an internal {idp_id} |
| 67 | + A->>Idp: https://sts.windows.net/{tenant-id}/.well-known/openid-configuration |
| 68 | + Idp->>A: {..., "jwks_uri": "jwks_uri"} |
| 69 | + A->>Idp: jwks_uri |
| 70 | + Idp->>A: {keys: [{"kid": kid}, ...]} |
| 71 | +
|
| 72 | + Note right of VM: A python script (pymongo.py) runs on boot<br>It reads the MongoURI from an env-var<br>The pymongo driver gets the JWT from the metadata endpoint<br>See description below |
| 73 | + VM->>Idp: http://169.254.169.254/metadata/identity/oauth2/token |
| 74 | + Idp->>VM: JWT token: {<br>"iss": "https://sts.windows.net/{tenant-id}", <br>"sub": "{vm_configured_identity}"<br>} |
| 75 | +
|
| 76 | + Note right of A: A database user exist on the organization<br>username: {idp_id}/{vm_configured_identity} |
| 77 | + VM ->> A: connect(JWT token, MongoURI) |
| 78 | + activate A |
| 79 | + A->>A: validate token and lookup database user |
| 80 | + A->>VM: auth ok |
| 81 | + VM->>A: insert record into database |
| 82 | + A->>VM: insert record response |
| 83 | + VM->>A: close connection |
| 84 | + deactivate A |
| 85 | +``` |
| 86 | + |
| 87 | +- (5) We use terraform to configure a [`user_assigned_identity`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) for the VM and use the [metadata endpoint](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http) to obtain the token |
| 88 | + - See also [MongoDB pymongo docs.](https://www.mongodb.com/docs/languages/python/pymongo-driver/current/security/enterprise-authentication/#mongodb-oidc) |
| 89 | + - The python script is configured by [cloud init](https://cloudinit.readthedocs.io/en/latest/reference/examples.html#writing-out-arbitrary-files) using `custom_data` on the [VM (`linux_virtual_machine`)](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_virtual_machine) |
| 90 | +- (10) Can be configured with terraform variables: |
| 91 | + - `insert_record_database` |
| 92 | + - `insert_record_collection` |
| 93 | + - `insert_record_fields` |
| 94 | + |
| 95 | +## Usage |
| 96 | + |
| 97 | +**1\. Ensure your Azure credentials are set up.** |
| 98 | + |
| 99 | +1. Install the Azure CLI by following the steps from the [official Azure documentation](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli). |
| 100 | +2. Run the command `az login` and this will open your default browser and perform the authentication. |
| 101 | +3. Once authenticated, it will print the user details as below: |
| 102 | + |
| 103 | +```shell |
| 104 | +⇒ az login |
| 105 | +You have logged in. Now let us find all the subscriptions to which you have access... |
| 106 | +The following tenants don't contain accessible subscriptions. Use 'az login --allow-no-subscriptions' to have tenant level access. |
| 107 | +XXXXX |
| 108 | +[ |
| 109 | + { |
| 110 | + "cloudName": "AzureCloud", |
| 111 | + "homeTenantId": "XXXXX", |
| 112 | + "id": "XXXXX", |
| 113 | + "isDefault": true, |
| 114 | + "managedByTenants": [], |
| 115 | + "name": "Pay-As-You-Go", |
| 116 | + "state": "Enabled", |
| 117 | + "tenantId": "XXXXX", |
| 118 | + "user": { |
| 119 | + |
| 120 | + "type": "user" |
| 121 | + } |
| 122 | + } |
| 123 | +] |
| 124 | +``` |
| 125 | +
|
| 126 | +**2\. Ensure your MongoDB Atlas credentials are set up.** |
| 127 | +
|
| 128 | +This can be done using environment variables: |
| 129 | +
|
| 130 | +```shell |
| 131 | +export MONGODB_ATLAS_PUBLIC_KEY="xxxx" |
| 132 | +export MONGODB_ATLAS_PRIVATE_KEY="xxxx" |
| 133 | +``` |
| 134 | +
|
| 135 | +**3\. TFVARS** |
| 136 | +
|
| 137 | +Create a file named `vars.auto.tfvars` |
| 138 | +
|
| 139 | +```hcl |
| 140 | +project_name = "tf-example-oidc" |
| 141 | +location = "eastus" # Azure region |
| 142 | +owner = "my-user" |
| 143 | +org_id = "YOUR_ATLAS_ORG_ID" # e.g 65def6ce0f722a1507105aa5 |
| 144 | +region = "US_EAST_1" # Atlas region |
| 145 | +insert_record_database = "test" |
| 146 | +insert_record_collection = "test" |
| 147 | +ssh_public_key = "ssh-rsa AAA...." # see below for how you can configure this |
| 148 | +``` |
| 149 | +
|
| 150 | +Configuring the `ssh_public_key` for the Azure VM: |
| 151 | +
|
| 152 | +```shell |
| 153 | +cd ~/.ssh |
| 154 | +ssh-keygen -t rsa -b 4096 -C "<[email protected]>" # to generate a keypair for the VM |
| 155 | +export TF_VAR_ssh_public_key=$(cat ~/.ssh/id_rsa.pub) # set the `ssh_public_key` with an env var instead of using the variable |
| 156 | +``` |
| 157 | +
|
| 158 | +**4\. Import the `mongodbatlas_federated_settings_org_config`** |
| 159 | +
|
| 160 | +replace `{federated_settings_id}` and `{org_id}` and run: |
| 161 | +
|
| 162 | +```shell |
| 163 | +terraform init |
| 164 | +terraform import mongodbatlas_federated_settings_org_config.this {federated_settings_id}-{org_id} |
| 165 | +``` |
| 166 | +
|
| 167 | +**5\. Review the Terraform plan.** |
| 168 | +
|
| 169 | +Execute the below command and ensure you are happy with the plan. |
| 170 | +
|
| 171 | +```shell |
| 172 | +terraform plan |
| 173 | +``` |
| 174 | +
|
| 175 | +**6\. Execute the Terraform apply.** |
| 176 | +
|
| 177 | +Now execute the plan to provision the resources. |
| 178 | +
|
| 179 | +```shell |
| 180 | +terraform apply |
| 181 | +``` |
| 182 | +
|
| 183 | +**7\. Connect to MongoDB to verify the record has been inserted.** |
| 184 | +
|
| 185 | +- Get the connection string by running `terraform output -json | jq -r '.user_test_conn_string.value'` |
| 186 | +- Open your preferred tool, e.g., [MongoDB Compass](https://www.mongodb.com/products/tools/compass) |
| 187 | +- You should see a new record inserted in `{database}` (default name is `test`), `{collection}` (default name is `test`), e.g.: `{"_id": "6661790007beeb09e3f1b914", "hello": "world", "ts": "2024-06-06T08:53:20.125919"}` |
| 188 | + - `{database}` depends on `insert_record_database` Terraform variable |
| 189 | + - `{collection}` depends on `insert_record_collection` Terraform variable |
| 190 | + - It can be up to 2 minutes after `terraform apply` finishes for the record to be inserted |
| 191 | +
|
| 192 | +**8\. Destroy the resources.** |
| 193 | +
|
| 194 | +- Once you are finished your testing, ensure you destroy the resources to avoid unnecessary Atlas and Azure charges. |
| 195 | +- Note that the organization will be unlinked from the [Federated Authentication](https://www.mongodb.com/docs/atlas/security/federated-authentication/#federation-management-console). |
| 196 | + - Use `terraform state rm mongodbatlas_federated_settings_org_config.this` to avoid this |
| 197 | +
|
| 198 | +```shell |
| 199 | +terraform destroy |
| 200 | +``` |
| 201 | +
|
| 202 | +## Next Steps |
| 203 | +
|
| 204 | +- Change `pymongo_oidc.sh` to run your own app, start your `systemd` service, etc. |
| 205 | +- Integrate with your identity provider of choice, ideas: |
| 206 | + - Github for accessing your Atlas Cluster from a Github Action |
| 207 | + - Gitlab for accessing your Atlas Cluster from a Gitlab Job |
| 208 | +- For more information, see [MongoDB Atlas Workload OIDC docs](https://www.mongodb.com/docs/atlas/workload-oidc/#prepare-your-external-identity-provider). |
| 209 | +
|
| 210 | +## Troubleshooting |
| 211 | +
|
| 212 | +### Debugging the VM Instance |
| 213 | +
|
| 214 | +1. Get the ssh command for connecting: `terraform output -json | jq -r '.ssh_connection_string.value'` and connect. |
| 215 | +2. Verify the python script exist: `ls -la ~` should show `pymongo_oidc.py`. |
| 216 | +3. Check the output log from cloud-init: |
| 217 | + - `sudo cat /var/log/cloud-init.log` |
| 218 | + - expect to find log lines where the script result is logged: `/var/lib/cloud/instance/scripts/part-002` (could have a slightly different name) |
| 219 | + - `sudo cat /var/lib/cloud/instance/scripts/part-002` |
| 220 | + - `sudo cat /var/log/cloud-init-output.log` |
| 221 | + - the log should end with something similar to: |
| 222 | +
|
| 223 | +```log |
| 224 | +... more log content |
| 225 | +Installing collected packages: dnspython, pymongo |
| 226 | +Successfully installed dnspython-2.6.1 pymongo-4.7.3 |
| 227 | +WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv |
| 228 | +creating client with uri=mongodb://looooong-connectionstring.mongodb.net:27017/?ssl=true&authSource=admin&replicaSet=atlas-xgvzij-shard-0&authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:https%3A%2F%2Fmanagement.azure.com%2F # same as output "user_oidc_conn_string" |
| 229 | +inserting into test test, record: {'hello': 'world', 'ts': '2024-06-06T08:53:20.125919'} |
| 230 | +insert response: InsertOneResult(ObjectId('6661790007beeb09e3f1b914'), acknowledged=True) |
| 231 | +script complete |
| 232 | +Cloud-init v. 24.1.3-0ubuntu1~22.04.1 finished at Thu, 06 Jun 2024 08:53:21 +0000. Datasource DataSourceAzure [seed=/dev/sr0]. Up 111.45 seconds |
| 233 | +``` |
0 commit comments