# install.packages("pak")
# pak::pak("keyring")
library(keyring)
The Challenge
How many times have you needed to enter an API token or perhaps credentials to a service account? Have you ever seen someone save credentials inside of a script? It happens, I’ve seen it - I hate to admit it, but I’ve probably done it myself. It’s easy to do when it’s crunch time. You tell yourself, you’ll remove them when you’re done testing - but, gasp - it’s too late, you’ve mindlessly committed and pushed those changes on up to the remote with git.
Or maybe you’re new and just getting started with R. You’re almost certainly going to need to inject a credential or something similar sooner or later. You don’t have to leave these in your scripts. There is a better way!
The Solution
There is an R package out there that makes it just as easy not to hardcode credentials into scripts. That package is keyring
!
Let’s take a look.
Installing keyring
I prefer to use pak
to install R packages, so I will often install that first and then use it to install the desired packages.
Putting keyring
into practice
keyring
stores credentials using the default credential manager for your operating system. keyring
makes interacting with that manager, inside of your code, fairly seamless.
# keyring can be run interactively with pop ups
# or you set the values in code - execute the code
# and then remove those lines if anything is going to be saved
# save some useful credentials
key_set_with_value(
service = "my-service",
username = "my-username",
password = "my-super-secret-password-SSSHHHH"
)
# interactively - you would call `key_set("my-service")`
# and fill in the details in the pop up.
Remember - you're just executing the code and not saving the
plain text password or senstive information in the code in a real world situation.
Now how would we access those credentials for later use? The service
and username
previously entered becomes the identifiers used to pull the credential back into your environment.
# if you happen to forget your services
# you can list them all out!
::key_list() keyring
service username
1 my-service my-username
Notice how the usernames are listed alongside the service? Users should be aware of that if they do not want usernames showing up in the console output. However, we’ll exploit that functionality later on. All a logged in user needs to retrieve a credential is a service name and a username.
Typically keyrings are set per user and are thus subject to whatever security is employed around the user account. It is imperative that users take care to secure their accounts, otherwise one compromised account can quickly spill over into others.
Let’s grab that credential now.
key_get(service = "my-service", username = "my-username")
[1] "my-super-secret-password-SSSHHHH"
So the code above just prints straight to console - still not exactly what you would want in real life but now that can be saved to an object and used just about anywhere.
Examples
Let’s walk through some plausible examples:
# remember - interactively we'd be using key_set("<service name here>")
key_set_with_value(
service = "open data portal",
username = "me@asenetcky.dev",
password = "mytotallyrealpassword123"
)
# sometimes you just want to use the service name
# and the password - and the "password" may
# not even be a password per se.
key_set_with_value(
service = "definitely real sql server connection string",
password = "127.0.0.1"
)
Helper Functions
Keyring works great for little private/internal helper functions and packages that you might write or contribute to in your line of work. Why not wrap a helper function around some keyring functionality?
I am using renv
- for dependency management and I think you should too but that can be the topic of another post. I am going to assume the reader is not using renv
and list the libraries - but I may miss one because renv
has spoiled me. Be sure to check our renv
and its excellent documentation.
# pak::pak(
# c(
# "dplyr", # for tidyverse data manipulations
# "purrr", # for funtional programming - and in our example, error catching
# "glue", # for easy formatted strings
# "checkmate", # for common checks in functions
# "rlang" # great for core language helpers
# )
# )
# if we use `renv` and don't mind using the full function name - package::function
# you can avoid these library statements entirely.
library(dplyr)
library(purrr)
library(rlang)
library(glue)
library(checkmate)
Our dependencies are set up - I’ll use the full function names so that there will be no ambiguity about what function comes from where.
<- function(service_name) {
nab_service_cred # check user input
# `checkmate` is great to testing function input
# and/or putting together unit tests in packages
::assert_character(service_name)
checkmate
# handle global bindings
<- username <- NULL
service
# grab email
<-
email ::key_list() |>
keyring::filter(service == service_name) |>
dplyr::pull(username)
dplyr
# throw error if empty
if (purrr::is_empty(email)) {
::abort(
rlang::glue("service: '{service_name}' - credential not found")
glue
)
}
# grab password
<-
password ::key_get(
keyringservice = service_name,
username = email
)
# return a named list
::lst(
dplyr
email,
password|>
) # probably best to return
# invisibily in case of unintended prints
invisible()
}
This little helper can be a building block for other functionality in your scripts or package. Maybe you have a process upstream that handles errors elegantly - you can then wrap this up in purrr::safely()
and then handle potential errors at your convenience.
<- purrr::safely(
safer_nab
nab_service_cred,# set some default or placeholder values in case of errors
otherwise = dplyr::lst(
email = "default-or-fake@email.com",
password = "default-or-fake-password.com"
) )
# test it out
<- safer_nab("not-a-service")
results $result results
$email
[1] "default-or-fake@email.com"
$password
[1] "default-or-fake-password.com"
# the show goes on!
# but if we want to see the error - we still can.
$error$message results
service: 'not-a-service' - credential not found
# look there is our error message!
# what about our service from before?
<- safer_nab("open data portal")
results results
$result
$result$email
[1] "me@asenetcky.dev"
$result$password
[1] "mytotallyrealpassword123"
$error
NULL
Please keep in mind that keyring
is very local to the user, and computer they are using. It is not a replacement for some more heavyweight solutions. However, it doesn’t cost users anything to use, it’s licensed under the permissive MIT license so it can generally be incorporated into codebases, and it is easy to use and readily available. So for simple setups and/or simple projects I cannot really think of a reason not to use it.
Hopefully these examples highlight how keyring can be a great tool to bolster security around credential handling in code, as well as a building block for helper functions that can get your team on the same page with connections, databases, service accounts and other credentials.
Cleaning up
Now we have all these fake services and credentials in our operating system’s credential manager. How do user clean it all up? keyring
has tools for that as well.
Users can use keyring::key_delete()
to wipe out credentials they no longer want stored.
# jog our memories about the services...
::key_list() keyring
service username
1 open data portal me@asenetcky.dev
2 definitely real sql server connection string
3 my-service my-username
# oh yeah - these ones.
::lst(
dplyr"my-service",
"open data portal",
"definitely real sql server connection string"
|>
) # let's borrow from our helper function
# I'm feeding the service names into our helper function
# so we can keep the service name and the email needed
# for the deletion in `purr::walk()`
::map(
purrr
\(serv) {<- nab_service_cred(serv)
cred $name <- serv
cred
cred
}|>
) ::walk(
purrr
\(cred) {<- purrr::pluck(cred, "name")
name <- purrr::pluck(cred, "email")
email ::key_delete(service = name, username = email)
keyring
}
)
::key_list() keyring
[1] service username
<0 rows> (or 0-length row.names)
Your turn
If you haven’t already, check out keyring
and see what use cases you can come up with.
Citation
@online{senetcky2025,
author = {Senetcky, Alexander},
title = {Managing {Credentials} with Keyring},
date = {2025-04-23},
url = {https://asenetcky.dev/posts/2025-04-23-keyring/},
langid = {en}
}