X

Best Practices from Oracle Development's A‑Team

Using the OCI Instance Principals and Vault with Python to retrieve a Secret

Tim Melander
A-Team Cloud Solution Architect

Introduction

Imagine a Magician that can read your mind and suddenly pulls a secret out of your head you never told them. This is kind of what I am going show how to do, but using OCI magic. Take the following use case.

  1. A program running on a compute instance needs to access an external service.
  2. To access the external service the program needs a username and password.

In point 2 above the username is not necessarily a secret, but the password is. One option is too hard code the password in the program, not an uncommon approach. But is that really a good idea? I think or at least hope we can all agree the answer is — NO!! In this article I will show an alternative option to store a password or any other secret in a secure way by leveraging a new OCI feature called Instance Principals, and in combination with another new feature called the Vault we have a great way to solve this problem without a lot of effort. Interested? Let's go!

Introducing Instance Principals & More

The Instance Principals is something unique to OCI where the compute instance itself can be authorized to make API calls to other services. No password needed too retrieve the secret! It's the magic of the Instance Principals at work. 

Kiran Thakkar’s wrote a blog Secure way of managing secrets in OCI that covers concepts around the Vault that includes some bonus Python examples which one shows how to retrieve a secret. 

To add even more control, we can use a policy that can restrict a program running on a compute instance to only retrieve a specific secret from a Vault. By combining all three features, Instance Principals, Vault, and policy, we have a much better approach to allowing a program to access sensitive information. So let's do it! 

Getting Started

The following summary lists what is need to make all this work. Follow each section in the order you see it to be successful. The effort will be worth it.  

  1. Create a Compartment
  2. Create a Dynamic Group
  3. Create a Vault with a key and secret
  4. Create a Policy using the Dynamic Group
  5. Create Compute Instance in the Compartment
  6. Install Linux packages – python 3.6, pip 20.0.2, and oci-cli 2.10.0
  7. Create Python script on the Compute Instance to retrieve the secret

Create a Compartment

A compartment is a logical container to organize and control access to OCI resources. In our case we need a compartment for our Compute instance Vault. If you already have a Compartment feel free to skip to the next section. Use the following steps to create a compartment.

  1. Login to the OCI Console as an Administrator
  2. Go to Menu > Identity > Compartments
  3. Click the Create Compartment button
  4. Enter the following:
    • Name: my-compartment
    • Description: My Compartment
    • Select a Parent Compartment
  5. Click Create Compartment
  6. Go into the new Compartment and copy the OCID; this will be used next.

Create a Dynamic Group

A Dynamic Group is a group that dynamically grant access to resources based on a rule. Our Dynamic Group will be used with a matching rule to determine which instances we want to allow API calls against the service we are going to use. The following rule is an example, but could easily be modified to meet other requirements. Use the following steps to create a dynamic group.

  1. Login to the OCI Console as an Administrator
  2. Go to Menu > Identity > Dynamic Groups
  3. Click the Create Dynamic Group button
  4. Enter the following:
    • Name:  my-secret-group
    • Description: My Secret Dynamic Group
    • Rule:  any {instance.compartment.id = ‘<ocid_compartment>’}
      • NOTE:  Where <ocid_compartment> is the ocid copied from my-compartment created earlier
  5. Click the Create Dynamic Group button to save
ALERT:  Use alternative rules found in Managing Dynamic Groups to provide even greater restrictions to resources if needed.  

 

Create a Vault with a key and secret

We will now create a Vault in the compartment created earlier, then add a key that will be used to encrypt a new secret. The secret could be anything, but for our example we will store a password. Note that you could add multiple secrets if needed. Using the following steps to create a vault, a key, and a secret.

  1. Login to the OCI Console as an Administrator
  2. Go to Menu > Security > Vault
  3. Select the compartment my-compartment created earlier or an existing 
  4. Click the Create Vault button
  5. Enter the following:
    • Name:  my-vault
  6. Click Create Vault button to save
  7. Click on the my-vault that was just created
  8. Click on the Keys link under Resources 
  9. Click Create Key button
  10. Enter a Name for the key; e.g. my-vault-key
  11. Select 256 bits from the Key Shape
  12. Click Create Key button to save
  13. Click on the Secrets link under Resources
  14. Click Create Secret button
  15. Enter the following:
    • Name:  my-secret
    • Description:  My Secret
    • Encryption Key:  select my-vault-key created earlier
    • Secret Contents:  <my secret here>
  16. Click Create Secret button
  17. Click on the secret “my-secret”
  18. Copy the secret OCID to be used next.

 

Create a Policy using the Dynamic Group

A policy provides a way to control access to resources. In the OCI documentation in section Creating a Dynamic Group and Matching rules, it gives an example that is really meant to allow administrators to manage Vaults, Keys, and Secrets, which grants a lot of control; i.e. 

NOTE:  POLICY FOR ADMINISTRATORS

allow dynamic-group my-group to manage secret-family in tenancy
allow dynamic-group my-group to manage vault in tenancy
allow dynamic-group my-group to manage keys in tenancy

However, I am going to rein back the above statement rules to restrict our Python script (created later) running on an instance to only be allowed to retrieve a specific secret using a single statement with a combination of Verbs, Resource-Types, and General Variables. Now compare my example:

NOTE:  BETTER POLICY TO ONLY ACCESS A SECRET

allow dynamic-group my-secret-group to read secret-family in compartment my-compartment where target.secret.name = 'my-secret'

If we decompose the above single statement we can see how it provides stricter access to our secret which you can appreciate. 

  1. Using a group to dynamically restricts all instances to a specific compartment
    allow dynamic-group my-secret-group
  2. Using a verb only allow read access to the secret-family
    to read secret-family
  3. Using a resource-type to limit us to a specific compartment
    in compartment my-compartment
  4. Using a variable to limit us to a specific secret name "my-secret"
    where target.secret.name = 'my-secret'

Feel free to use combinations of Dynamic GroupsVerbs, Resource-Types, and General Variables to build your own restrictions. Use the following steps to create a the example policy.

  1. Login to the OCI Console as an Administrator
  2. Go to Menu > Identity > Policies
  3. Click the Create Policy button
  4. Enter the following:
    •  Name: my-secret-policy
    • Description: My Secret Policy
    • Statements
      • allow dynamic-group my-secret-group to read secret-family in compartment my-compartment where target.secret.name = 'my-secret'
  5. Click the Create button to save

Create Compute Instance in the Compartment

In the last two sections we need to install some Linux packages and then create a script, but before we do we need a Compute instance. If you already have a Compute instance you can skip this step. If you need to create a Compute instance these are very basic steps to create a Linux instance. Use the following steps to create a compute instance if needed.

  1. Login to the OCI Console as an Administrator
  2. Go to Menu > Compute > Instances 
  3. Click Create Instance button
  4. Use the following example:
    • Name:  linux
    • Image:  <leave default Oracle Linux 7.8 or select Oracle Linux 6.10>
    • Change Shape:  <pick your shape>
    • Configuring networking:  pick your VCN, Subnet Compartment, and Subnet
    • Add SSH keys: Add your ssh rsa public key
  5. Click Create button to create the instance.
     

Install Linux packages – python 3.6, pip 20.0.2, and oci-cli 2.10.0

With the compute instance created previously or if you already had a compute instance, ssh into the instance as opc. Use the next three sections to install Python 3.6, pip 20.0.2, and oci-cli 2.10.0 or later. 

Install Python 3.6 –

By default, the OCI Linux 6 or 7 images currently at the time of this post include Python 2.7.5, but to work with the newer secret features we need minimally 2.7.9. If we follow the latest requirements of the OCI CLI SDK we should be using Python 3.5 or greater https://docs.cloud.oracle.com/en-us/iaas/Content/API/Concepts/cliconcepts.htm#Requirements. The following approach to installing a newer version of Python allows us to maintain any older version of Python yet still let us run a newer version for our purposes. Python 3.6 will be installed in its own directory where it can be referenced as needed. 

  1. Should return Python 2.7.5
    python --version
  2. The following installs Python 3.6
    sudo yum install -y rh-python36
  3. Open a new bash shell using Python 3.
    scl enable rh-python36 bash
  4. Should now return Python 3.6
    python --version
  5. Modify .bashrc to use Python 3 on next login
    vim ~/.bashrc    
  6. Enter "i" to insert, paste in the following, and save .bashrc.
    # Added to source Python 3 to login environment
    source scl_source enable rh-python36
        

Install pip 20.0.2 –

By default, Python comes with pip 9.0.1, but we need version 20.0.2.

  1. Returns pip 9.0.1
    pip --version
  2. Upgrade pip to 20.0.2
    pip install --user --upgrade pip
  3. Should now return pip 20.0.2
    python -m pip -- version

Install oci-cli  >= 2.9.11 –

This will install the latest oci cli. At the publishing of this article 2.10.0 was the latest.

  1. Installs and upgrades to latest oci cli
    python -m pip install oci-cli --upgrade
  2. Should now return oci cli >= 2.10.0
    oci --version

Create Python script on the Compute Instance to retrieve the secret

Finally, we can create a script to retrieve our secret. The following steps creates a Python script that you can use as a framework to build on, but this could also be done in other languages that are supported such as Java, Ruby, and Go — Software Development Kits and Command Line Interface. Use the following steps to create a Python script with the given example.

  1. Terminal into the Linux instance created earlier and create a file.
    vim get-secret.py
    
  2. Press "i" and paste in the following Python script.
    #!/usr/bin/env python3
    # coding: utf-8
    # COPYRIGHT (c) 2020 ORACLE A-TEAM
    # THIS SAMPLE CODE IS PROVIDED FOR EDUCATIONAL PURPOSES OR
    # TO ASSIST YOUR DEVELOPMENT OR ADMINISTRATION EFFORTS AND
    # PROVIDED "AS IS" AND IS NOT SUPPORTED BY ORACLE CORPORATION.
    # License: http://www.apache.org/licenses/LICENSE-2.0.html
    
    import oci
    import base64
    import sys
    
    # Replace secret_id value below with the ocid of your secret
    secret_id = "ocid1.vaultsecret.oc1.<my_secret_ocid>"
    
    # By default this will hit the auth service in the region the instance is running.
    signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
    
    # In the base case, configuration does not need to be provided as the region and tenancy are obtained from the InstancePrincipalsSecurityTokenSigner
    identity_client = oci.identity.IdentityClient(config={}, signer=signer)
    
    # Get instance principal context
    secret_client = oci.secrets.SecretsClient(config={}, signer=signer)
    
    # Retrieve secret
    def read_secret_value(secret_client, secret_id):
        response = secret_client.get_secret_bundle(secret_id)
        base64_Secret_content = response.data.secret_bundle_content.content
        base64_secret_bytes = base64_Secret_content.encode('ascii')
        base64_message_bytes = base64.b64decode(base64_secret_bytes)
        secret_content = base64_message_bytes.decode('ascii')
        return secret_content
    
    # Print secret
    secret_contents = read_secret_value(secret_client, secret_id)
    print(format(secret_contents))
    
  3. Be sure to change the secret_id ocid in the get-secret.py script with your secret, then save and exit.
    secret_id = "ocid1.vaultsecret.oc1.<my_secret_ocid>"
    
  4. Make the get-secret.py script executable.
    chmod +x get-secret.py
    

Testing the Script

Testing the script is pretty simple. If it does not work go back and make sure each step was followed carefully.  

  1. Run the following to make sure it returns Python 3.6.
    python --version
  2. Run the following command to magically return the secret.  
    ./get-secret.py
    
    <my secret here>
    

Summary

In summary there are several use cases where this method can be used. Manasi Vaishampayan wrote a great blog Going beyond TCP healthchecks with OCI Load Balancer that uses a program to access an external service, which uses a username and password.

What is used in this article could be leveraged to secure the password in a Vault. The benefit are the Instance Principal and Vault helps abstracts the complexity of building your own security approach to store and encrypt things like passwords or other sensitive information. Let OCI store the sensitive data in an encrypted Vault and then retrieved seamlessly using the magic of the Instance Principal and then throw in a tightly controlled policy to boot. If that were not secure enough, OCI also rotates the certificates in the Vault used to encrypt the data several times a day. If you want more even more, the Vault secret adds additional controls called Secret Rules that can for example expire a secret; see more on that here in Rules for Secrets

I hope this article is useful and to learn more on both of these features please see more about Instance Principals in Calling Services from an Instance and the Overview of Vault

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha