Abstracting Credentials from Python Scripts

Python has rapidly grown into one of the most popular languages for automation. That’s why LogicMonitor supports Python when it comes to managing your LM portal and expanding your monitoring coverage with scripts. During your scripting adventures, you may find yourself wanting to take your work to the next level. One widely-applicable improvement is credential abstraction.

Defining Abstraction and Its Benefits

Simply put, abstraction in scripting hides a code implementation. Abstraction can have a number of benefits including:

  • Points of improved code legibility or reduced code execution complexity
  • Improved code reusability
  • Preparing interfaces

This is commonly seen when we talk about using modules or libraries in scripting. Someone else has done the heavy lifting, packaging their detailed work into a library. We as end users simply use the abstracted version of their work so that we can focus on higher level implementations. Part of why Python’s barrier of entry is so low is because of this common, architectural abstraction.

Use Cases for Abstracting Credentials

Let’s use the LogicMonitor REST API as an example. API credentials have a one-to-one relationship with their applicable portals: one set of credentials will work for only one LM portal. You might consider abstracting credentials if you:

  1. Wrote one script that I want to use across multiple LM portals without needing to save a new copy of the script for each portal.
  2. Want your script to work but only when executed in a specific environment as a means of helping control where the script is hosted.
  3. Backup your scripts to a version control system like a Git repository and do not want to save credentials directly in the backup copies.

Common Abstraction Options

Credentials and other potentially sensitive data are commonly abstracted out of scripts by retrieving their values from:

  1. The command line
  2. Environment variables
  3. Files

We will cover options 1 and 2. Note that each scripting and programming language will have a different syntax for these options. In Python, you would use argparse and os.getenv.

Many internet suggestions recommend Python’s os.environ method for retrieving environment variables; however, the os.environ method will throw an exception and prematurely end your script’s execution if any environment variable you try to access does not actually exist. With os.getenv, you can define a backup value so that if the environment variable doesn’t exist, a default value will automatically substitute. It’s a win-win situation in my opinion.

Example Python Script for Abstracting Credentials

# Author: Jeoffri Davis
# Purpose:
#       - Example of abstracting LM REST API credentials
# Tested With:
#       - Python 3.7
#       - MacOS Mojave
# Requires:
#       - Python module(s): argparse
#       - LogicMonitor API credentials (access id and access key)
# Inputs:
#       - LogicMonitor API credentials (access id and access key)
# Outputs:
#       - Echoes credentials to stdout
import argparse
import os
def parse_command_line_arguments():
   """
   Parse arguments from command line and return results in an object.
   Abstracting credentials out of the script can improve flexibility in
   changing credentials and security.
   Using argparse:
       https://docs.python.org/3/howto/argparse.html
   Getting OS env vars while also specifying a default value:
       https://docs.python.org/3.7/library/os.html#os.getenv
   """
   # Define hash table with argument help strings
   arg_help = {
       'description': 'This script demonstrates a way \
           to avoid hard coding credentials. Abstract \
           out credentials by retrieving them from the \
           command line, or, fall back to OS env vars. \
           Consider script execution in test and production. \
           In test, you may want the default output \
           verbosity, which prints output to standard out. \
           In production, you may want faster script \
           execution by using the -q or --quiet option, \
           which skips printing output to standard out.',
       'access_key': 'LogicMonitor REST API Access Key, \
           available from LM portal>Settings>Users & Roles>API Tokens',
       'access_id': 'LogicMonitor REST API Access ID, \
           available from LM portal>Settings>Users & Roles>API Tokens',
       'company': 'Company name for LogicMonitor portal',
       'quiet': 'Hide script output'
   }
   # Use argparse to read arguments
   parser = argparse.ArgumentParser(description=arg_help['description'])
   parser.add_argument(
       "-a", "--access-key",
       default=os.getenv('LM_ACCESS_KEY', 'Default key'),
       help=arg_help['access_key']
   )
   parser.add_argument(
       "-b", "--access-id",
       default=os.getenv('LM_ACCESS_ID', 'Default id'),
       help=arg_help['access_id']
   )
   parser.add_argument(
       "-c", "--company-name",
       default=os.getenv('LM_COMPANY_NAME', 'Default company'),
       help=arg_help['company']
   )
   parser.add_argument(
       "-q", "--quiet",
       action='store_true',
       help=arg_help['quiet']
   )
   args = parser.parse_args()
   return args
def main():
   # Abstract out credentials by retrieving them from
   # the command line, or, fall back to OS env vars
   credentials = parse_command_line_arguments()
   # Consider script execution in test and production.
   # In test, you may want the default output verbosity,
   # which prints output to standard out. In production,
   # you may want faster script execution by using the
   # -q or --quiet option, which skips printing output
   # to standard out.
   if not credentials.quiet:
       print("Access Key: " + credentials.access_key)
       print("Access ID: " + credentials.access_id)
       print("Company: " + credentials.company_name)
       print("Quiet: " + str(credentials.quiet))
if __name__ == '__main__':
   main()

Benefits of Command Line Parsers such as Python Argparse

Modern command line parsers generally offer a couple of benefits:

  1. Obtaining values either from the command line, or, predefined defaults
  2. Automatically generating script usage

Our example uses Python’s argarse in order to define values for specific information: LogicMonitor credentials–access key, access id, and company–as well as a verbosity option called “quiet”. These are variables whose values aren’t hard coded directly in the script. Rather, the values could come from interfacing with another source of information because the values are passed into, or “abstracted out of”, the script. Using a second script as wrapper, we can apply this one, original copy of the Python script to multiple LM portals. We could backup copies of the script once or regularly without needing to remove credentials first. If we didn’t supply any explicit credentials through the command line, we could leverage environment variables defined at the host’s operating system level or lower such as a terminal session or container. This means the same script would fail if we moved to a different environment and attempted to execute it without having the environment variables in place.

Scripts that use their language’s command line parser can normally be called with a “-h”, “–help”, or equivalent option in order to see the automatically generated script usage. Python argparse, Groovy CliBuilder, and PowerShell comment based help are examples of where this is possible.

Examples of Python Script Execution and Output

Note that in the examples below “ATX-JDAVIS:python_abstract_credentials jdavis$” is a command line prompt and “abstracted_credentials.py” is the file containing our example code.

Obtaining Python Script Usage Help

Here is an example of executing our script with Python argparse’s “–help” option in order to see the script usage automatically generated by argparse. 

ATX-JDAVIS:python_abstract_credentials jdavis$ python3 ./abstracted_credentials.py --help
usage: abstracted_credentials.py [-h] [-a ACCESS_KEY] [-b ACCESS_ID]                                 [-c COMPANY_NAME] [-q]
This script demonstrates a way to avoid hard coding credentials. Abstract out credentials by retrieving them from the command line, or, fall back to OS env vars. Consider script execution in test and production. In test, you may want the default output verbosity, which prints output to standard out. In production, you may want faster script execution by using the -q or --quiet option, which skips printing output to standard out.
optional arguments:
  -h, --help            show this help message and exit
  -a ACCESS_KEY, --access-key ACCESS_KEY
                        LogicMonitor REST API Access Key, available from LM portal>Settings>Users & Roles>API Tokens
  -b ACCESS_ID, --access-id ACCESS_ID
                        LogicMonitor REST API Access ID, available from LM portal>Settings>Users & Roles>API Tokens
  -c COMPANY_NAME, --company-name COMPANY_NAME
                        Company name for LogicMonitor portal
  -q, --quiet           Hide script output
ATX-JDAVIS:python_abstract_credentials jdavis$

Execution with Command Line Arguments

In this example, we define values for our command line arguments rather than relying on defaults. We could call this script multiple times, sequentially or in parallel, while also supplying different inputs. The script itself; however, would remain unchanged as all values are abstracted out.

ATX-JDAVIS:python_abstract_credentials jdavis$ python3 ./abstracted_credentials.py -a "I used the short-hand command line option of 'a' to supply my own access key" --access-id "My Custom Access ID" -c "LogicMonitor"
Access Key: I used the short-hand command line option of 'a' to supply my own access key
Access ID: My Custom Access ID
Company: LogicMonitor
Quiet: False
ATX-JDAVIS:python_abstract_credentials jdavis$ 

Execution with Default Command Line Argument Values

Here is an interesting example of executing our script while retrieving a mix of values from environment variables and predefined defaults.

 
ATX-JDAVIS:python_abstract_credentials jdavis$ python3 ./abstracted_credentials.py
Access Key: %c3)j}]5_5mZ6gF)QkN$6-zV4L+[9Qd32V!{%n(}
Access ID: k9NzWaIQFImBu36aSWP8
Company: Default company
Quiet: False
ATX-JDAVIS:python_abstract_credentials jdavis$

We did not supply command line arguments and the order in which our script checks for credentials is command line, environment variables, and then defaults. My developer environment had environment variables defined for access key and id, but not for company or quiet. The script, therefore, automatically used the default values for company and quiet.

Execution with Quiet Option

An unexciting but necessary example of suppressing script output. Note that the quiet option operates like a switch. If it’s present then the assumed value is a Boolean “True”; otherwise, the assumed value is a Boolean “False”.

ATX-JDAVIS:python_abstract_credentials jdavis$ python3 ./abstracted_credentials.py --quiet
ATX-JDAVIS:python_abstract_credentials jdavis$

You typically want this quiet option in production environments where you won’t watch the script execution in real time. Writing the output to a log, which isn’t covered in this article, is a good companion to suppressing prints to standard out.

Implementation Risks

No implementation is perfect in all ways. Often we make tradeoffs, striving for key benefits while either mitigating or accepting unfavorable consequences. Some of the risks of abstracting credentials as we demonstrated include the following.

Code Legibility and Maintainability

Ironically, code complexity increases specifically where command line parsing is implemented. In our example, one would need to understand Python argparse in order to maintain that portion of the script. This is an unavoidable tradeoff in order to achieve increased script flexibility.

False Sense of Significantly Heightened Security

Reduced credential visibility is better than none. On the other hand, it can lead some people to believe their script has immediately become exponentially more secure. If security were your primary driver, then abstracting credentials out of scripts would be a critical first step; however, you would not stop there because any process reporting tool will be able to capture and report any command line options passed to a script.

For example, in macOS and Linux environments you could insert a wait time into our example script, execute the script in one terminal, and then execute the ps -ef command in another terminal in order to capture the running Python process. The ps command may return an entire screen worth of results. You can filter down the response by piping the results into grep, which will let you search for particular strings. You can see below that our credentials were abstracted but not entirely hidden because we’re still able to find our example script executing with an access key of “Can you find this?”.

ATX-JDAVIS:~ jdavis$ ps -ef | grep 'Can you find this?'
  501 61873 61739   0 10:44PM ttys000    0:00.04 /Library/Frameworks/Python.framework/Versions/3.7/Resources/Python.app/Contents/MacOS/Python ./abstracted_credentials.py --access-key Can you find this?
  501 61979 61575   0 10:45PM ttys005    0:00.00 grep Can you find this?
ATX-JDAVIS:~ jdavis$

For increased security, research obfuscation and encryption. While noteworthy topics, they are not the focus of this article.

Conclusion

This article illustrated Python argparse, one of many examples of how one can abstract credentials from scripts. Abstraction allows scripts to become flexible and scale in a number of ways. We reviewed an example that removed the one-to-one relationship between a Python script that leverages the LM REST API, and the number of portals to which wanted to apply the script. We touched on increasing script complexity in one place in order to improve the overall user experience. Finally, we clarified that abstraction does not equate to immediate security improvements.