A newer safer way to access the dashboard API

PhilipDAth
Kind of a big deal
Kind of a big deal

A newer safer way to access the dashboard API

This should be a blog post, but I don't have such a facility here.  So grab a coffee and put your phone on silent.

 

Introduction

I propose the following to allow for the safe storage of credentials and configuration information for scripts and programs accessing the Meraki dashboard API and to allow that information (such as the API key) to be shared across scripts so that this sensitive information is not written into the code all over the place.

 

I propose the most sensitive information such as API keys (DB passwords,etc) be stored in the users home directory in a file called .meraki.env.  This makes it available to all programs and scripts.  By moving sensitive information completely outside of the script and the directory it is in makes that information safe by default when people publish their work (such as to github).

It will also make it easier for new users as once they have used one script they will be able to download any script and try it without having to repeat the same configuration information again and again.  Meraki simple.

 

I propose project related configuration be stored in a file called .env.  This file could potentially contain sensitive information as well.  This file should be excluded if the project is published.  This might be accomplished by listing it in the .gitingore file.

Commonly projects include a .env.example file to show what fields are needed.  This example file should be published.

 

I also propose that environment variables be allowed to be used to override any other parameter in these two files.  This allows the easy use of serverless environments such as Amazon AWS Lambda as well as allowing overides between different environments (such as test and production).

 

Specifically, I propose information is collected in the following order.  The first occurance of the information takes precedence in case it appears lower down in the order.

  • Environment Variables
  • Local .env file
  • Global .meraki.env file in the users home directory

The .env and .meraki.env files will have lines of the format:

variable=value

 

I propose certain variable names be adopted as a standard.  So far I propose only one:

x_cisco_meraki_api_key

I propose this value because it is referenced in the developer API examples.

 

For several popular languages there is a module called dotenv that is well regarded and very popular.  I have prepared examples showing how using this module allows you to make only a tiny change to your code to gain this safety.

 

My .meraki.env file in my home directory contains:

x_cisco_meraki_api_key=put your top secret API key

My .env file in the directory I am running the below examples from contains:

orgName=Sample Org

 

Python

Lets take a look at Python.  First you need to install the required modules.

 

 

 

 

pip install meraki-sdk
pip install -U python-dotenv

 

 

 

 

I have taken the example from the developer SDK and modified it slightly to be able to use all of this niceness I have been talking about.  The main differences are the first 4 lines - yep just 4 lines.  And then all you have to do to reference the API key is reference os.getenv("x_cisco_meraki_api_key").  The last line gives an example of referencing a config item from the local .env file to the project.

 

 

 

 

import os

from dotenv import load_dotenv
load_dotenv()
load_dotenv(dotenv_path=os.path.join(os.path.expanduser("~"),".meraki.env"))

from meraki_sdk.meraki_sdk_client import MerakiSdkClient
from meraki_sdk.exceptions.api_exception import APIException

meraki = MerakiSdkClient(os.getenv("x_cisco_meraki_api_key"))

orgs = meraki.organizations.get_organizations()
print(orgs)

print("Sample orgName parameter="+os.getenv("orgName"))

 

 

 

 

Was that not Meraki simple?

 

Node.js

Now onto node.js.  You should do an "npm init" to create your new project.  Then you need to install the modules:

 

 

 

 

npm install meraki
npm install dotenv

 

 

 

 

 Now I have taken the example from the developer SDK and modified it slightly to be able to use all of this niceness.  

 

 

 

 

const os = require('os');
const path = require('path');

require('dotenv').config()
require('dotenv').config({ path: path.join(os.homedir(),'.meraki.env') })

const meraki = require('meraki');
const configuration = meraki.Configuration;

configuration.xCiscoMerakiAPIKey = process.env.x_cisco_meraki_api_key;

meraki.OrganizationsController.getOrganizations().then(function(res) {
		console.log(res)
		console.log("Sample orgName parameter="+process.env.orgName)
	}
);

 

 

 

 

The main differences are the first 4 lines - yep just 4 lines (does it sound like I am repeating myself?).  And then all you have to do to reference the API key is reference process.env.x_cisco_meraki_api_key.

 

Was that not Meraki simple?

 

Powershell

Powershell is not at this point in time "Meraki simple".  So this one is a bit longer.

 

First I cut my own dotenv.

 

 

 

 

function load_dotenv {
	[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')]
	param($file=$args[0])

	if ( Test-Path $file) {
		$content = Get-Content $file -ErrorAction Stop

		foreach ($line in $content) {
			if ($line.StartsWith("#")) { continue };
			 	
    	if ($line.Trim()) {
      	$line = $line.Replace("`"","")
        $kvp = $line -split "=",2
        if ($PSCmdlet.ShouldProcess("$($kvp[0])", "set value $($kvp[1])")) {
        	$key=$kvp[0].Trim()
        	if (-not (Test-Path env:\$key)) {
        		[Environment]::SetEnvironmentVariable($key, $kvp[1].Trim(), "Process") | Out-Null
        	}
        }
      }
    }
  }
}

 

 

 

 

Then I cut my own version of get_organizations().

 

 

 

 

function get_organizations() {
	$header_org = @{
  	"X-Cisco-Meraki-API-Key" = $env:x_cisco_meraki_api_key
  	"Content-Type" = 'application/json'
	}
	
	$url='https://api.meraki.com/api/v0/organizations'

	Invoke-RestMethod -Method Get -Uri $url -Headers $header_org
}

 

 

 

 

 And then the code to pull it all together.

 

 

 

 

load_dotenv(".env")
load_dotenv($env:USERPROFILE+"\.meraki.env")

get_organizations

Write-Output("Sample orgName parameter="+$env:orgName);

 

 

 

 

To reference the API key is as simple as referencing $env:x_cisco_meraki_api_key.  A bit more long winded - but once you have the dotenv function in place it is pretty simple.

 

Summary

What I have presented is an OS and platform agnostic way of safely storying credentials and configuration information outside of the main scripts and code in a safe way that allows the sharing of information between scripts such as the API key rather than having to duplicate it in every script.  The solution also allows per invocation overides for severless environments or where multiple environments are used (such as dev, test and production).

11 REPLIES 11
BrechtSchamp
Kind of a big deal

And that is why @PhilipDAth is the onspoken leader of the All Stars 😁!

 

Well done mate. I'd still like to see a way to store the data in a non-plaintext way though. But that'll be hard to do in a cross-language and cross-platform way.

>I'd still like to see a way to store the data in a non-plaintext way though.

 

The problem is to encrypt cross platform would require another key to protect the first key - and now you have a circular issue.  It could be obfuscated, but I was trying to stick to well known systems that I new were available on all platforms, and that also required a minimal change to peoples existing code to reduce the barrier to adoption.

 

I chose the home directory on purpose.  Typically the permissions should be set so that only the user has access to it.

 

On Linux I would go a step further and configure it with mode 400 (which makes the file read only, and only the "owner" can access it).

Modern Linux's like Ubuntu encrypt the users home directory or place it on an encrypted volume.  So that should take care of that one.

And hey, if the home directory is safe enough to store the users private SSH keys, it is safe enough to store the Meraki API key.

 

Windows users tend to be "looser" with their security (I feel it is just a bit harder to do on Windows so people don't bother).  In an ideal world they would do the same thing, revoke acess to all user accounts except their own and only allow read access.

Good points! It kind of depends on the usecase. If the script has to be automated, you'll indeed run into the circular issue. However, if it's a script that you manually start, then you could leverage keyrings to fetch the password.

Any chance this can be pinned to the top of this subforum?

Avis
Meraki Employee
Meraki Employee

@PhilipDAth  this is awesome.  Thank you so much for sharing.  I definitely learned a new way to handle api keys.

 

That said, I did an issue when trying to reference the variable in the local .env file.  After a bit of troubleshooting, I think there is a  typo.  Your local dir .env variable is listed as "orgname" but the last line of your script has "orgName".  Shouldn't they be the same?
 
PhilipDAth
Kind of a big deal
Kind of a big deal

It wasn't case sensitive on the platform I am using - but I have made the case all the same now just in case.

I do support the idea of storing the api keys in an environment file.

However using

os.path.expanduser("~")

is a bad idea as this works only under linux. The data ist stored in different places on other platforms.

 

I've created a similiar approach for a smart home library.

 

def get_config_file_locations() -> []:
    search_locations = ["./config.ini"]

    os_name = platform.system()

    if os_name == "Windows":
        appdata = os.getenv("appdata")
        programdata = os.getenv("programdata")
        search_locations.append(
            os.path.join(appdata, "homematicip-rest-api\\config.ini")
        )
        search_locations.append(
            os.path.join(programdata, "homematicip-rest-api\\config.ini")
        )
    elif os_name == "Linux":
        search_locations.append("~/.homematicip-rest-api/config.ini")
        search_locations.append("/etc/homematicip-rest-api/config.ini")
    elif os_name == "Darwin":  # MAC
        # are these folders right?
        search_locations.append("~/Library/Preferences/homematicip-rest-api/config.ini")
        search_locations.append(
            "/Library/Application Support/homematicip-rest-api/config.ini"
        )
    return search_locations

 

The function will return 3 file locations depending on the OS.

  • ./config.ini -> File in the current working directory
  • the location for a userbased configuration
    • Windows: %APPDATA%\homematicip-rest-api\config.ini
    • Linux: ~/.homematicip-rest-api/config.ini
    • MAC: ~/Library/Preferences/homematicip-rest-api/config.ini
  • the location for a systemwide configuration file
    • Windows: %PROGRAMDATA%\homematicip-rest-api\config.ini
    • Linux: /etc/homematicip-rest-api/config.ini
    • MAC: /Library/Application Support/homematicip-rest-api/config.ini

 

Your comment about "os.path.expanduser("~")" only working in Linux is incorrect.  It works perfectly in Windows.

 

It is in the official documentation.

https://docs.python.org/2/library/os.path.html#os.path.expanduser

"On Unix and Windows, return the argument with an initial component of ~ or ~user replaced by that user’s home directory."

Technically yes it is working and you will get the home directory, but "~" isn't the place on windows to store configurations.
Settings for users should be placed in %APPDATA% which would be "~\AppData\Roaming" on Windows 7 and higher
Take a look at this msdn blog post from 2010.

Those directories don't really address it well.  They assume you are storing settings per "application", while I am wanting to store configuration settings (like the users credentials or a common org name) for the user across all applications.

 

Amazon AWS also uses the same approach on Windows as I have suggested, by storing the credentials and configuration in the Windows home directory.

I've found their approach to work well, so I cloned that good working example.  Also it is very simple, uses a minimum of code and works across all platforms.

Thats now currently a definition of "application". In this case I would define it as the "api layer" and store the data in %APPDATA%/meraki_sdk/meraki.env on windows.

If this will be added directly into the sdk, then you could do "meraki.load_config()" and it has the same "short code" effect for every script you are running.

Sorry to say that, but just because other people are using the home directory wrong, that doesn't mean everyone has to do it. I know many applications which are missusing the home directory on windows for the configuration store. 99% of them are linux applications which are ported to windows.
Get notified when there are additional replies to this discussion.
Welcome to the Meraki Community!
To start contributing, simply sign in with your Cisco account. If you don't yet have a Cisco account, you can sign up.