When developing applications, handling sensitive information like credentials securely is paramount. Hardcoding passwords or API keys directly into your scripts is a significant security risk. Instead, several methods can be employed to safeguard these credentials. This post focuses on using keyring for credential storage in Python, while also comparing other common methods like .env files and environment variables, highlighting their shortcomings.
Using keyring for Credential Storage
The keyring library in Python provides a secure way to handle passwords and other secrets. It interfaces with the operating system's credential storage mechanisms, such as Keychain on macOS, Windows Credential Locker on Windows, and GNOME on Linux.
Benefits of Using keyring
Security: Credentials are stored securely using the operating system’s native methods, which are designed to protect sensitive information.
Convenience: keyring abstracts away the complexity of handling encryption and secure storage.
Cross-Platform Support: It works seamlessly across different operating systems, providing a consistent API.
Single change point: Multiple scripts or applications can use the same credential, reducing change points when passwords change. This also makes it easier to synchronize changes in centralized password vaults.
Flexible: Both passwords and API token keys can be stored.
User-Defined: The credentials are related to the user account, so they are re-usable for the given user, and specific to that user.
Setting Up keyring
To start using keyring, you need to install it:
pip install keyring
Storing and retrieving credentials is straightforward:
import keyring as kr
service_name = 'db_connection'
user_name = 'db_login'
pwd = 'pwd123'
# Store a credential
kr.set_password(service_name, user_name, pwd)
# Retrieve a credential
password = kr.get_password("service_name", "username")
print(f"password: {password}")
#pwd123
Multiple usernames may be used in association with a given service name. But a username does not need to be specified when using the get_password or get_credential function. Using the set_password for an existing credential will automatically update it.
You can also go into the system interface to manage the credentials:
Sample usages:
By Service: For applications that are meant to be environment independent, you may want to associate a single username with the service.
import keyring as kr
service_name = 'db_connection'
user_name = 'db_login'
pwd = 'pwd123'
# Store a credential
kr.set_password(service_name, user_name, pwd)
# Retrieve application credential
cred = kr.get_credential(service_name,"")
cred_username = cred.username
cred_password = cred.password
print(f"Application Name:{service_name}; User Name: {cred_username }; Password:{cred_password }")
#Application Name:db_connection; User Name: db_login; Password:pwd123
By Service Stack: There may be multiple credentials for a given service stack.
import keyring as kr
service_name = 'api-set'
t1_token_name = 'token1'
t1_token = 'a23405439jrkj$2jwkl2k'
t2_token_name = 'token2'
t2_token = 'j39!kwljwl3n2110j'
# Store a credential
kr.set_password(service_name, t1_token_name , t1_token)
kr.set_password(service_name, t2_token_name , t2_token)
# Retrieve application credential
cred1 = kr.get_credential(service_name, 't1_token')
t1_cred_username = cred.username
t1_cred_password = cred.password
cred2 = kr.get_credential(service_name, 't2_token')
t2_cred_username = cred.username
t2_cred_password = cred.password
By Environment: It is also possible set up credentials for different environments.
import keyring as kr
import argparse
def getCreds(service_name, userName=""):
try:
cred = kr.get_credential(service_name, userName)
credUserName = cred.username
credPassword = cred.password
print(f"Service Name:{service_name}; User Name: {credUserName}; Password:{credPassword}")
return credUserName, credPassword
except:
print(f"Credentials not found for Service Name: {service_name} and User Name:{userName}.")
if __name__ = '__main__'
# Store a credential
kr.set_password('Connections Dev', 'db_user', 'abc123')
kr.set_password('Connections UAT', 'db_user', 'mno567')
kr.set_password('Connections Prod', 'db_user', 'xyz890')
parser = argparse.ArgumentParser()
parser.add_argument('--environment', choices=['Dev', 'UAT', 'Prod'], default='Dev', help='Select an environment')
args = parser.parse_args()
service_name = f"Connection {args.environment}"
db_user_name, db_pwd = getCreds(service_name,'db_login')
Comparing with Other Methods
While researching options I was perplexed at how much these other methods were mentioned. Especially given the potential issues surrounding them.
.env Files
A .env file is a text file that contains key-value pairs of environment variables. These files are typically used in conjunction with libraries like python-dotenv to load environment variables into a Python application.
Why .env Files are Not Ideal
Accessibility: Anyone with access to the server or directory can read the .env file, exposing sensitive credentials.
Version Control Risk: It's easy to accidentally include .env files in version control systems, potentially exposing secrets to the public.
Management Complexity: Managing .env files across different applications on a server can be cumbersome.
Credential Changes: Any change in credentials requires updating the .env file across all applications, increasing the risk of inconsistencies.
Environment Variables
Environment variables are another common method for storing sensitive information. They are set at the operating system level and accessed by the application at runtime.
Why Environment Variables are Not Ideal
Limited Security: While environment variables are not stored in the codebase, they can still be accessed by anyone with the right permissions on the server.
Visibility: Environment variables can be viewed by other processes running on the same system, potentially leading to accidental exposure.
Manageability: They can create clutter, and lack proper capabilities to manage them.
Bonus Security
For systems that require heightened security to restrict password or tokens from being exposed should someone gain access to the server account, you can further hash and encrypt the credentials when storing and retrieving them in your applications.
Conclusion
Using keyring for credential storage in Python provides a more secure and manageable approach compared to .env files and environment variables. By leveraging the operating system’s native secure storage, keyring minimizes the risk of credential exposure and simplifies credential management across different environments. While .env files and environment variables are commonly used, they pose significant security risks and management challenges that keyring effectively addresses.
Adopting keyring in your Python projects can enhance the security of your applications and provide a more robust method for handling sensitive information.
Comments