★ Releases ★ Changelog ★ Issues ★
A lightweight, stand-alone, headless secret service tool backed by a Keepass v2 database.
Rook allows you to use a KeePass v2 database as storage for secrets. It provides client and server modes; the server unlocks the database and stays in memory; the client communicates over a socket with the server and fetches data. This allows:
There are other ways of accomplishing each of these, but none that accomplish all of them. passhole and kpmenu are close alternatives. passhole is Python with a lot of dependencies to audit. Recent versions of kpmenu also have large dependency trees, and the mode of operation makes it harder to use for non-GUI scripting.
Rook is not compatible with any other secret service tools; it does not communicate over, or rely on, DBus, or anything systemd. It should work as well on Darwin, *BSD, or any other POSIX-ish OS as it does on Linux, although this has not been tested. I don't know what it'll do on Windows.
Rook is mostly complete. Some features are missing; see the Roadmap section below. It might crash, although it's been a while since I've seen one. It should be currently able to replace most secret-service type calls you might make from scripts; you can easily fetch passwords by account with the client, which is what you really need.
rook help
prints a list of the supported commands. Start a rook server process with rook serve
, and then access it by calling rook
again with one of the other commands. There is no configuration, or configuration file.
Exit the rook server with rook exit
or ^c
. If, for some reason, it leaves behind the socket file, the next time you start rook it'll complain about one already running. You can override that warning with the -f
argument.
Rook will watch the DB file and auto-refresh the memory cache if the file changes.
Start a rook server with:
rook -f serve -P /path/to/your.kdbx
-f
here forces rook to ignore and overwrite any pre-existing socket file; you probably shouldn't use it unless you are certain rook isn't already running. -P
tells rook to prompt for the database password. -P
can be used either when starting a server, or when calling open
from a client. Alternatively (and, probably, more normally), start the rook server with no password, and unlock it with a client call when you can prompt for a password:
$ rook serve /path/to/kdbx &
$ rook open -P
If you use a key file for a second security factor, you can specify it with the -k
argument to either serve
or open
.
Or, the server can be called with no database, and the client can tell it which database to open by passing it a path; in this case, it must be a path the server can find the file through, usually a fully qualified path. However, if the rook server was run in the same directory as one or more databases, the client can provide a simple relative path:
$ rook open -P my.kdbx
A database supplied by open
replaces whatever the server was started with.
As an alternative to all of password prompting, rook will also take a password from the ROOK_PASSWORD
environment variable. The prompt takes precedence. Remember that passwords are plain text, and environment variables are -- while reasonably secure -- still more persistent and accessible than other mechanisms. The password may also be provided to the serve
command via stdin pipe with the --stdin
argument; this allows starting a Rook server with a password vault without exposing the password through environment variables and without requiring user interaction. This means you could also do this in an interactive bash, as an alternative to -P
:
$ rook serve --stdin database.kdbx
supersecretpassword
^d
It's possible to tell whether the database is unlocked by making most any other request, e.g. info
, as the server will return a NO OPEN DATABASE
error message. If no database is open, the lock
and commands
requests will not throw errors, and the only other request that will work is open
.
Databases can be live-reloaded with the reload
command; this assumes the credentials have not changed,and it simply replaces the in-memory database from the current version on disk. There is not yet -- and may never be -- fancy DB merging such as KeepassXC does; it's a straight-up replacement.
If you want to move the socket location, e.g. to /var/run/user/$(id -u)/rook.sock
then use the --socket
argument. If you do this, you must set it for both the server and every client call. Easiest would be to create an alias in your shell's rc file, but you can also just specify it everywhere you call Rook.
If the entry Title is unique, then show
only needs that; otherwise, it'll show all the matching entities -- which may not be what you want when using rook for script authentication purposes. In the case of duplicates, provide the path - separated by /
- to the entity -- or part of it. For example, if you Keepass database looks like:
Root
Internet
Accounts
me
Server
Accounts
me
Identities
me
myself
then rook show me
will show of all three me
entries; rook show Accounts me
will show two of them; rook show myself
will show only the one entry; and rook show Server/Accounts me
or rook show Identities me
will show only the one entry. A fully qualified path would be rook show Root/Internet/Accounts me
, but rook requires only enough to uniquely identify entries.
Rook can also display your database as a tree by using the --tree
argument:
$ rook ls -t
┌─ /
├─┬─ Subgroup 1
│ ├─┬─ Subsubgroup 2
│ │ └── Subsub item
│ └── Subgroup 1 item 1
├── First entry
├── Second test
├── Third entry
├── fourth entry
└── Something wicked
Empty groups are not shown; leaves in the tree are always entries.
Databases can be hard-locked with the lock
command, e.g. rook lock
. With a hard lock, rook forgets everything it knows about the database, except for the path on the filesystem. To unlock the database, a client call of rook open -P
is required —- ths will prompt for the DB password.
Databases can also be soft-locked by calling lock --pin
; in this case, the rook server will generate a 4-digit one-time pin and return it to the client. This pin can be used one time to unlock the database with open --pin <digits>
. If open --pin
is called incorrectly 3 times, the database is hard-locked. Any other calls to rook while the database is in this state are soft-failures, in that they return an error, but do not count as failed unlock attempts. This prevents cron-jobbed scripts looking up paswords from hard-locking the database, and since none of the requests can be used to brute-force rook, they're considered mostly harmless. Similarly, attempts to call lock --pin
while the database is locked also soft-fail.
If a database is soft-locked, a client call to open -P
(providing the full password) will also unlock the database.
Rook does not have any built-in support for timing-out and locking databases. This can be accomplished with cron, or systemd timer units. Since the intended purpose of rook is to provide secrets to cron jobs, such as email and calendar syncing, there is no "timeout after disuse period" functionality. However, in some cases a more ideal behavior can be tied into the screen locker; an example is provided in (Examples)[#Examples], below.
rook list
shows all entries in the DB, with an index number and a path. While the index can be used, it's not a fixed thing and could change as the database changes, so it's best to not rely on it in scripts.
The alias in the following example isn't necessary; it's just to keep rook running everything in ${PWD}
.
Here are some code snippets from various config files:
mbsync (~/.config/isyncrc
)
PassCmd "rook show -p 'Email account'"
aerc (~/.config/aerc/accounts.conf
)
source-cred-cmd = rook show -p Accounts 'Email account'
vdirsyncer (~/.config/vdirsyncer/config
)
password.fetch = ["command", "rook","show","-p","CardDAV account"]
msmtp (~/.msmtprc
)
passwordeval rook show -p Identities "SMTP server"
restic You could store all of your restic backup information in Keepass:
RESTIC_PASSWORD=$(rook show -p Accounts/Backups glamdring) \
RESTIC_REPOSITORY=$(rook show -f restic_repository Accounts/Backups glamdring) \
EXCLUDES=($(rook show -f excludes Accounts/Backups glamdring)) \
/usr/bin/restic backup ${EXCLUDES[@]/#/-e } --one-file-system \
$(rook show -f files Accounts/Backups glamdring)
Searches look in several places for matches: titles, URLs, and tags. However, show
only matches against titles. Both search
and show
also take an optional path as the first argument, which filters the entities before matching; e.g.:
$ rook ls
1 Accounts Entry 1
2 Accounts Entry 2
3 Internet Entry 1
4 Internet Entry 2
$ rook search 'Entry 2'
Index: 2
Path: Accounts
Title: Entry 2
Index: 4
Path: Internet
Title: Entry 2
$ rook search Internet 'Entry 2'
Index: 4
Path: Internet
Title: Entry 2
If you're looking at rook, you may have a similar system, as I describe here, all of these functions are usually provided by the DE; if you're replacing one, you've probably replaced all of them.
On my system, I use xss-lock as an auto-locker, although xautolock works just as well. In both cases, the locker called is a shell script that wraps i3lock; I do this because there are things I want to happen when the screen locks, such as disabling dunst messaging. In this script, you can call rook lock --pin
, get the pin and save it, and then use the pin to unlock rook when the screen is unlocked.
A simplified version of what I run is:
$ xss-lock ~/.bin/superlock
$ cat ~/.bin/superlock
#!/usr/bin/zsh
dunstctl set-paused true
lock() {
i3lock-fancy-rapid 16 3 -enkS 1
rook open --pin $1
}
lock $(rook lock --pin)
dunstctl set-paused false
or,
rook lock --pin | (
i3lock-fancy-rapid 16 3 -enkS 1
read pin && rook open --pin $pin
)
Just be careful about using long-term environment variables to store the pin, because environment variables can be read from /proc
and it increases the attack surface. For example, I'd avoid:
PIN=$(rook lock --pin)
i3lock
rook open --pin $PIN
There are shell scripts in the utils/
directory: autotype.sh
and getAttr.sh
. autotype.sh
can provide form-filling utility; it works by grabbing the focused window's title and uses rook match
to find the best match in the DB -- rook uses the Keepass Autotype rules for this, if they exist -- and the best match is returned with the first line being the defined autotype sequence for the entry. The script then parses out the key/value pairs and uses xdotool
to type the sequence. getAttr.sh
uses rofi to allow the user to select an entry from the DB, and then an attribute from the entry, and then it performs a user-requested action with the data.
autotype.sh
and getAttr.sh
are shell scripts; if you use them, you should also read through them. They're not exactly trivial scripts, but at ~100 lines long each they're auditable even if you're not a shell expert. Again, I trust me implicitly, but you shouldn't.
autotype.sh
is straightforward -- it just does its thing: tries to find the best entry and type the autotype sequence. If it gets multiple matches, it shows them in a rofi dialog, you choose one, and off it goes.
getAttr.sh
has a -h
argument for help, and more complex options. It can use fzf instead of rofi
, and be used entirely from the command line. It can copy the selected value to the system clipboard, print the value out, or type the value out with xdotool
.
These are dependencies only for the utility scripts -- the rook binary itself does not use any of these.
autotype.sh
work the harder way in bash.autoType.sh
to put values in the clipboard.KeepassXC autotype sequences have a lot of keywords; the script understands a small number of these -- ENTER, DELAY, TOTP, USERNAME, PASSWORD. It should be easy to add to this list by modifying the case switch at the end of the file -- improvement patches are welcome.
To use the script, put it in your path and bind it to a hotkey, e.g.
herbstluftwm keybind Mod4+t spawn ${HOME}/bin/autotype.sh
### OR, if you installed with a package manager:
herbstluftwm keybind Mod4+t spawn /usr/bin/rook-autotype
If you installed rook with one of the binary distribution packages, and you have the necessary required programs listed above (rofi, ripgrep, yad, etc.), then the utility scripts will be installed in /usr/bin/
as rook-autotype
and rook-getattr
.
There's a shell script in utils/
called getAttr.sh
which shows a list of all entries, and when one is selected shows a list of all attributes, one of which can be selected. If called with the -t
argument, the selected value will be typed using xdotool
. If called with the -c
arg, the value will be copied into the clipboard using xsel
. If no argument is given, the script prints the value to STDOUT.
getAttr.sh
can prompt either with rofi
or fzf
; the -f
argument selects fzf
, and rofi
is used by default. Using fzf
allows getAttr
to be used without a GUI.
You can see how autotype.sh
will work by faking an input dialog with yad
. To get as close to real life, you'll want to run the application that will ask for the login -- if it's Paypal, then go to Paypal.com in an incognito window and click "Logn". If it's some GTK application, run the application and start the login process. Then use xprop
to get the window title -- this is what autotype.sh
, and incidentally, KeepassXC, uses for entry matching. Then run yad
with that window title as the --title
parameter -- something like this:
yad --form --field "Username" --field "Password:H" --field "TOTP:NUM" --title "Log in to Sourcehut"
Give that Username field focus and trigger your autotype -- once you hit OK, the values will be printed on the command line.
rook packages are available in Apline testing, and in Arch AUR. For Alpine, you'll have to have the testing repo enabled:
echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories
apk add --no-cache rook
For Arch, use your AUR enabled tool:
yay -Sy rook
Download the latest release from the releases page. Optionally, also download the binary signature and validate the binary. Decompress the binary with gzip
, copy it somewhere in your path, and off you go.
I provide builds for Linux arm64 and amd64, and the latest version as RPM, deb, apk, and Arch zst packages for each of these.
You can get my public key either from Sourcehut, or from one of the public key servers. You'll want to grab the one that says:
Sean E. Russell (Signing key for Sourcehut builds) <ser@ser1.net>
, which is key 5E0D7ABD6668FDD1
. To get it from Sourcehut, where the builds are made:
curl https://meta.sr.ht/~ser.pgp | gpg --import
### OR ###
gpg --keyserver keys.openpgp.org --recv-keys 6668FDD1
### AND THEN ###
gpg --verify rook_arm64_latest.gz.sig
If you install from one of the pre-built packages, the script autotype.sh
will be installed as /usr/bin/rook-autotype
, and getAttr.sh
as /usr/bin/rook-getattr
.
If you want to try building and running on something other than Linux -- say, one of the BSDs, or Darwin -- then this or the [next section](#Install the Go way) is what you'll need to use.
hg clone https://hg.sr.ht/~ser/rook
go build .
# OR, to embed the correct version s.t. `rook version` works,
go build -ldflags "-X main.Version=$(hg log -r tip --template '{bookmarks}')" .
go install ser1.net/rook@latest
# OR, if you know the version you want
go install -ldflags '-X main.Version=v0.0.9' ser1.net/rook@v0.0.9
The repo contains an nfpm.yaml
file, from which can be built packages for Arch, rpm, apk, and deb systems. You could use this to build old versions, or versions for other architectures such as arm5/6/7. If you want to do this, you're best off reading the nfpm usage docs, since repeating them here would be silly. But, basically, you'll need to make sure some environment variables are set, and run (to get an apk; replacable with rpm
, deb
, or archlinux
):
VERSION=$(hg log -r tip --template '{bookmarks}') \
GOOS=$(go env GOOS) \
GOARCH=$(go env GOARCH) && \
nfpm pkg -p apk
Rook's threat model is simple: it doesn't expose anything to any network connection, but does trust that the local system is secure. Don't use Rook if you don't trust the root user, or worry that someone could masquerade as your user. If your user or root accounts are compromised, then an attacker can talk to the Rook server over the socket and get at your secrets. Since the socket is created with user-only RW permissions, and is created in the user's home directory, it should still be safe on multi-user systems -- with the aforementioned caveat that root has access to everything.
Rook attempts to keep external dependencies to an absolute minimum. Rook itself is not a large program, and is only multiple files for semantic encapsulation. Aside from the Rook code itself, dependencies are:
net
imports. It, itself, depends onnet
So, in all of this, Rook should be the only thing using net
, and would be the only thing (if I weren't me) I'd be concerned about. rook needs net
to create the local socket over which the client/server communicate, and there are only two places rook opens a connection, both of which are to a local socket.
Regardless, as with any secret services, if you don't trust your own system, don't run these. Don't run KeePassXC at all, because with enough effort, root can still get memory dumps and extract secrets from your open database.
--secure
)Security can be improved by running rook in secure mode, by starting the server with --secure
. In this mode, the server will print out a 6-digit pin that the client will always have to provide for every interaction. Locking and unlocking the database will generate a new pin.
When run with --secure
, whatever provides the database credentials will print out the pin; if the server is run with -P
argument or with the ROOK_PASSWORD
environment variable, then the server will print the pin out on STDOUT. If the DB is locked and unlocked, or if the client open
s the database with the credentials, then the pin will be returned to the client. If the server is run in secure mode, the client will look in the environment variable ROOK_PIN
for the pin; this is the only way to pass the pin to the client.
The secure mode unfortunately makes scripting less useful, as every automated script now has to prompt the user for the pin whenever a call is made, and either cache it somehow or prompt the user every time. This can easily make rook less secure, if one of the scripts -- for example -- writes the pin to a cache file. This can be bypassed by updating a session environment variable, although this (again) re-weakens security as environment variables can be read by both root and any session logged in as the user. Also note that setting environment variables through a shell will usually expose the pin in any persistent shell history file.
If you want the DB to be locked periodically, run an external cron job that calls lock
. Rook might add a lock-after-inactivity function, since this can't really be done externally, but (a) it'd be useless to me since the purpose of rook is as a secret-service for other cron jobs like mail syncing, and so it'd never timeout and lock; (b) I'm really trying to limit the amount of code in rook, making it more auditable, and that means leaving out features; and (c) a general lock-after-time regardless of use is arguably more secure, and a more common feature in these sorts of tools -- and that can be done externally.
Rook itself does not keep the master password in memory in clear text, and neither does gokeepasslib. Variables that hold the password during credentialing are cleared, or are scoped out in brief functions.
Don't use the --password
argument. It's there to simplify testing, nothing more. If you use it, it could easily be stored in a shell history file, or even worse, be seen by any user in the sysetem through ps
.
Use of ROOK_PASSWORD
for the server is discouraged. While any account that can inspect the rook process' environment (via /proc/<pid>/environ
) can also probably dump memory and get at your secrets, by using the environment variable your master password is stored in plain text and this is less secure. It's less of a concern for client calls, because client calls are ephemeral.
The best way to minimize the master password's existence in memory in plain text is to run the rook server with at most the database path, and then use the open
command with the client, e.g.:
$ rook serve database.kdbx
$ rook open -P
Most of the roadmap is kept in the sourcecode. A good way of looking at it is to get legume and run leg in the source directory. However, there is a general thrust to most of what's coming:
There are countless giants that should be listed, that we all take so much for granted that they are unknown to most of us. However, I'd like to call out a couple of individuals whose code I've leveraged heavily, or sometimes (basically) stolen. Those I've copied/pasted from I've also done my best to audit in the process.
golang.org/x
package.rook is developed using a set of decentralized, CLI-based tools. Web tools are fine, and they're sometimes necessary, but they centralize data, are hard to script, and just generally get in my way. When there are many people from different domains collaborating on a thing, this extra impediment is necessary; when there aren't, it's not. I use code comments for issue tracking and legume to make that easier to view; I use changelog to generate the CHANGELOG.md.
rook was inspired by kpmenu, which approaches security differently, requiring interaction from the user on every request. This makes it harder to use as a secret service for automating scripts, and since kpmenu has a very specific threat model it's been resistant to patches that would weaken their solution, and even with those patches additional tools are needed to support autotype. As mentioned in Security, Rook assumes the user environment is secure and can therefore take a more relaxed approach; these are the same assumptions and approaches used by any secret service software, such as Gnome's secret-tool, keyring, or pass.
Linux users have many options for storing passwords. I can't list all of them here, but I can list the ones I've used, and why I believe rook was necessary. In every case where the tool has a bespoke data store, it means the user having to manually syncing passwords between Keepass and whatever the tool is using, and is for this reason is IMO unsuitable.
keyring
.gpg-agent
it's not adding much to your service load. It has two downsides: first, it's still a bespoke backend password database, meaning having to manually sync any passwords or entries with Keepass by hand; and second, it exposes a huge amount of metadata about the secrets. Since the entire DB is a filesystem, where the entry titles are directory names and attributes are files in the directory, the entire structure of the database is exposed, unencrypted, and readable. Attackers may not know what your Pornhub password is, but they know you have an account. This is fine for pass
, which is designed more for secret sharing across teams, and so the account names are not considered confidential information. It's pretty horrible for anyone else, and especially anyone considered a dissident for whom that metadata could be used in persecution.pass
databases. If you use pass, it's a pretty nice tool. pass
itself is a collection of shell scripts; gopass
is a single self-contained binary. It doesn't have any connection with Keepass, though.