Diagram showing two-layer token security with 1Password icon, encrypted vault, local key, river lines, arrows, and labels, Unlock, Encrypted Data, Decrypt, Local Key, and the title.

Building a Secure Token CLI in Python

Learn how to build a secure and reliable Python CLI for token management by combining encrypted storage and robust local key protection.



Introduction

Handling API tokens in CLI tools is a common task for any Python developer — but doing it securely is often overlooked or oversimplified. This article walks through a deep technical exploration of how to safely manage access tokens (like GitHub or AWS keys) in local development environments using Python.

You’ll follow a real-world reasoning path: exploring standard libraries (keyring, .env, op CLI), identifying their limitations, comparing threat models, and finally building a layered, pragmatic solution that balances usability and security.


The Problem

Tokens stored in plaintext or system-unlocked storage are vulnerable to silent reads from malicious code. Solutions like .env, environment variables, or keyring default backends are not enough:

  • Environment variables can be read by any process owned by the same user.
  • Keyring + Keychain on macOS unlocks secrets to all local scripts once the user logs in.
  • Encrypted file-based vaults prompt for a password every time, breaking usability.
  • Cloud-synced vaults, like 1Password, can be compromised remotely if improperly configured.

There is often a trade-off between usability and security — but they don’t have to be mutually exclusive.


Let’s bust a myth

“It’s just local — so it’s fine if it’s plaintext.”

That assumption is everywhere, and it’s dangerous.

Even in teams that use secure vaults like AWS Secrets Manager or HashiCorp Vault in production, the local development environment remains the weakest link — wide open and mostly ignored.

Being on your local machine does not equal being secure. If a token is in plaintext — in your .env, shell history, memory, or Keychain without user presence — it’s exposed.

My rule is simple

  1. Never store secrets in plaintext — not even locally.
  2. Use a vault — like 1Password — with biometric/user presence required.
  3. And don’t blindly trust even your vault.

Instead of storing the raw token, store an encrypted blob in your vault. That way, even if someone compromises your vault remotely, your token is still protected — because it was never there in plaintext to begin with.

This simple two-part model gives you protection at both ends:

  • The vault protects against local malware and session snooping.
  • The local decryption key ensures the vault alone isn’t enough.

No single piece is enough to access your secrets. That’s how security should be.


Option 1: keyring with macOS Keychain (Default Backend)

import keyring
keyring.set_password("mycli", "github-token", "ghp_xxx123")
token = keyring.get_password("mycli", "github-token")

Pros

  • Simple
  • Uses OS native Keychain
  • No manual password entry

Cons

  • Any process during login session can access the token
  • No prompt or user presence
  • High risk if malware is present locally

Option 2: keyrings.alt.file.EncryptedKeyring

from keyrings.alt.file import EncryptedKeyring
keyring.set_keyring(EncryptedKeyring())

Pros

  • Strong AES-256 encryption
  • Protects with a master password

Cons

  • Prompts for password every script run
  • No integration with system biometrics (Touch ID, Windows Hello)
  • Inconvenient for repeated CLI usage

Option 3: 1Password CLI (op) + Plaintext Token

subprocess.check_output(["op", "item", "get", "GitHub Token", "--field", "password"])

Pros

  • Touch ID + biometrics for access
  • Secrets encrypted and managed externally
  • Good developer experience (1Password handles UX)

Cons

  • Token is stored in plaintext within 1Password
  • Vault compromise = full secret leaked

Optimized Design: Layered Defense with Encrypted Token Blob

What if the 1Password vault only stores an encrypted blob, and your CLI handles the final decryption step with a local-only key?

Design

  • Store an AES or Fernet encrypted token blob in 1Password.
  • Keep a local-only key on disk (can be public or derived securely).
  • Require both vault access + local key to reconstruct the secret.
# Get encrypted blob from 1Password
output = subprocess.check_output([
    "op", "item", "get", "GitHub Token Enc", "--field", "password", "--format", "json"
], text=True)

# Local key for final decryption
key = open(".mycli_key", "rb").read()
cipher = Fernet(key)

token = cipher.decrypt(json.loads(output)["value"].encode()).decode()

Why This Works

Risk Mitigation
Vault leak (cloud, sync compromise) Encrypted blob stored — token not usable
Local malware 1Password prompts Touch ID before read
Local key theft Key is useless without unlocking 1Password

Tradeoff Accepted

  • Local key is public on disk (risk accepted)
  • 1Password provides the user auth gate
  • Token never exists in plaintext unless both conditions are met

Perfect security doesn’t exist — but this model balances practical usability with well-contained risk.


Value for Teams and Hiring

This design shows:

  • Security-first engineering thinking
  • Practical understanding of threat modeling
  • Deep knowledge of platform behavior (macOS Keychain, process access)
  • Ability to build layered defenses without impacting developer flow

Authored by Davi Luiz Guides
Part of the “Zero Trust Local Environment” series
Visit daviguides.github.io for more insights

Diagram showing two-layer token security with 1Password icon, encrypted vault, local key, river lines, arrows, and labels, Unlock, Encrypted Data, Decrypt, Local Key, and the title.