Introduction

Working at a financial institute ofcourse requires the use of using secure access to usernames and passwords and the like. For some period that had been out of reach for home consumers or smaller companies, but now with Hashicorp Vault, Ansible Vault, or smart access to applications like 1Password gives the opportunity to use these kind of smart access yourself.

Scope

This specific blog post, will focus on how I am using 1Password to access external applications, using their integration. If you use 1Password, you should have already setup the integration yourself. See this link to get started with setting up your own integration to one of your vaults.

I will however describe what I did -after setting up the above- to integrate that, and I will demonstrate that with an example script.

Definitions / explanations

1Password

To recap, if you are not familiar, 1Password started as a standalone application, that locally stored your usernames and passwords, and grew various options to automatically submit your credentials to a site, after authenticating to the app, etc. You can use it alone, but also share it with family members for a shy amount per month, or even company wide in case you are interested. Recently it grew a cloud only vault, that give the opportunity to centrally store application credentials and can be fetched from everywhere.

That does suggest a potential larger attack vector, then having a vault locally, but you cannot access the data otherwise. You should make the tradeoff for yourself.

Keyvaults

The concept behind keyvaults, is that is generally a ‘store secret data’ in a safe (aka vault). In some cases you can put something in and cannot (easily) get the information back. That is, if there are fine grained access controls in place that you can can prevent regular users from ‘fetching’ data from the vault, but only have automated access to that same vault. This makes it needless to store usernames and passwords in env files or in the code itself, you can retrieve it when needed and it will get logged etc.

A keyvault, nowadays, can also be queried by an API, that works for the cloud providers, and also 1Password has that option. You define a certain access method for your code and/or intermediate applications (like the 1Password-Connect service), and you can programatically query an endpoint that could result in a username, password, url, and what whatever you configure within the entry in the vault.

The example

The example below, is what I use daily to check whether the php group has a new release and released a new docker image so that I can rebuild my own customized web containers. I used to just fetch the tags and parsed them, but with the requirement to use api-v2 you also need authentication, and this script provides that easily with the help of 1Password.

The example uses the 1password connect sdk, a toolkit that talks to the 1Password Connect application that I run in a docker container.

So, here comes the script, together with 1Password integration:

#!/usr/bin/env python3

import requests, json, os, sys
from dotenv import load_dotenv

load_dotenv()

###### CONFIGURATION ITEM ##########

# Password module, either 1password or file
# Note that the .env file is required in both
# cases, but in one case we use it to connect to
# the 1password upstream, else we use the direct
# configuration from the file.
password_module = "1password"

if password_module == "1password":

    # Import modules required for 1password
    import onepasswordconnectsdk

    from onepasswordconnectsdk.client import (
        Client,
        new_client_from_environment,
        new_client
    )

    # Start a new 1password controller from environment
    client: Client = new_client_from_environment()

    # Setup the connection with the previous client section.
    # We look for a specific 'name of your item' in your application vault.
    # You can ofcourse set that in a variable or use it as parameter/cli param.

    config = onepasswordconnectsdk.load_dict(client, {
        "username": {
            "opitem": "name of your item",
            "opfield": ".username",
        },
        "password": {
            "opitem": "name of your item",
            "opfield": ".password",
        },
        "url": {
            "opitem": "name of your item",
            "opfield": "sitedata.url",
        },
    })

    # Items fetched from 1password record and assigned to variable
    base_url = config["url"]
    username = config["username"]
    password = config["password"]

    # End of 1Password information

elif password_module == "file":
    # Items configured in .env file
    base_url = os.getenv('BASE_URL')
    username = os.getenv('USERNAME')
    password = os.getenv('PASSWORD')

else:
    print("You need to configure a valid backend, we cannot proceed like this!\n")
    sys.exit("Please configure the right settings!")

image = sys.argv[1]
version = sys.argv[2]

# Set image login and tags url
login_url = base_url + '/v2/users/login'
tags_url = base_url + '/v2/repositories/library/' + image + '/tags/?page_size=10000'

###### END CONFIGURATION ITEM ##########

with requests.Session() as session:
    post = session.post(login_url, json={"username": username, "password": password})

    # Variable setting
    # Set token to token.
    token = post.json()['token']

    # Add the authentication token to the headers.
    headers['Authorization'] = "JWT {}".format(token)

    # create the tags list and fetch the proper url
    tags_list = session.get(tags_url, headers=headers)

    # refactor the response under tags_list as json and store it in json_response
    json_response = tags_list.json()

    # We only need the names in this purpose, loop through the results and match the
    # version we specified on the commmandline and then print it if there is a match.
    for name in json_response['results']:
        if version in name["name"]:
            print(name["name"])