VoidAccess
提出一个问题,VoidAccess 便并行搜索 12+ 个 Tor 搜索引擎、GitHub 与安全订阅源,然后返回可供 STIX、MISP 或 Sigma 使用的行动者、钱包、泄露凭证和 IOCs。Open so
AI 翻译
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Per-shell access to an encrypted SSH configuration directory.
You did the right thing: your SSH key has a good passphrase. Then you added it to
ssh-agent (or your desktop did it for you) so you would not have to type it forty times
a day — and from that moment the unlocked key is available to every process running as
your user, for as long as you are logged in:
curl and piped to /bin/sh,npm install postinstall hook, editor plugin and build script you run.None of them have to ask. $SSH_AUTH_SOCK points at the agent, and a process that was not
given the variable finds it in /proc/<pid>/environ of any of your shells. They cannot
copy the key, but they do not need to: they can sign with it, which means logging in
wherever you can and pushing wherever you can. The rest of ~/.ssh is no better guarded —
a key without a passphrase is just a file to read, one ProxyCommand added to
~/.ssh/config runs code the next time you connect, and an open ControlMaster
connection lets anyone run commands on that host without any key at all.
None of this is a bug. On Unix, the boundary is the user id, and everything you run is you.
Access is granted per shell, not per login. A shell that has run pidvault unlock, and
every process it starts, sees your decrypted config under /run/pidvault/stored/default/ and
can sign with the keys stored there — through pidvault's own agent, which answers only
processes of unlocked shells. The keys themselves cannot be read, not even from an
unlocked shell, so nothing running there can take them along. Every other process running
as you sees an empty directory and an agent with no keys. With several vaults, each shell
sees only the ones it unlocked: one shell can have your GitHub key and another your
production keys.
pidvault is not the perfect solution. It is an attempt to make this noticeably better without making SSH painful to work with: you unlock once per shell, and everything else behaves as before. It draws the line at the shell you unlocked, and a lot is still yours to get right:
curl | sh you run in it.
It cannot copy them, but it can change the vault, and whatever it plants there is still
there in the next shell you unlock. Keep a shell for SSH and do the risky things
elsewhere.ssh, or strange things in $PATH. Your own processes can write to
~/.local/bin, ~/.bashrc and the other files your shell trusts. An alias, a function
or a binary planted there runs inside the next shell you unlock, and pidvault cannot
tell it from the real thing.~/.ssh/config. It is editable the same way, and an entry
placed above it wins. sudo chattr +i ~/.ssh ~/.ssh/config stops that; authorized_keys
and known_hosts stay editable in place.unlock refuses unless kernel.yama.ptrace_scope is at least 1
(Ubuntu's default); even then the shell's ancestors, such as the terminal emulator, can.
On X11, any program on your display can read the passphrase as you type it.docker group, as most developers are: one
docker run --privileged --pid=host makes it root. The groups lxd, libvirt and
disk, and sudo without a password, do the same. Rootless Docker and Podman do not:
their containers have only your own powers, and are not started by your shell, so the
vaults stay closed to them. In the docker group pidvault still keeps your secrets from
programs that read the files they know, but not from one that sets out to get past it.The full list, with what each guarantee rests on, is in docs/security-design.md.
For Ubuntu 24.04 or later, on amd64, from the project's signed apt repository:
sudo curl -fsSLo /etc/apt/keyrings/pidvault.asc https://torstei.github.io/pidvault/pidvault.asc
echo "deb [signed-by=/etc/apt/keyrings/pidvault.asc] https://torstei.github.io/pidvault stable main" \
| sudo tee /etc/apt/sources.list.d/pidvault.list
sudo apt update && sudo apt install pidvault
The repository is signed with the key with fingerprint
D1434A919B60EEC6384F3CB716EFFB5096BD7539; check it with
gpg --show-keys /etc/apt/keyrings/pidvault.asc before trusting it. The same key is in
this repository as packaging/pidvault-apt.asc. Each .deb on the
releases page also carries a build
attestation: gh attestation verify pidvault_*.deb --repo torstei/pidvault shows that it
was built by this repository's release workflow, and from which commit.
Installing starts the service. Upgrading restarts it, which locks every shell.
pidvault was called sshvault up to 0.3.1. Its apt repository moved with the name, so the old one gets no more updates. Swap the source; the signing key is the same:
sudo rm /etc/apt/sources.list.d/sshvault.list /etc/apt/keyrings/sshvault.asc
then follow Installing. Installing pidvault removes sshvault and takes over its vaults,
which keep their passphrases. What is left is the paths in your own files. Replace the
sshvault block at the top of ~/.ssh/config with the output of pidvault stub, then, in
a shell with every vault unlocked:
sed -i -e 's|/run/sshvault/|/run/pidvault/stored/|g' -e 's|/run/sshvault-agent|/run/pidvault-agent|g' \
/run/pidvault/stored/*/config
The sshvault account stays behind, unused; sudo userdel sshvault removes it.
Stored vaults moved from /run/pidvault/NAME to /run/pidvault/stored/NAME, beside
/run/pidvault/ephemeral. Replace the block at the top of ~/.ssh/config with the output
of pidvault stub, then, in a shell with every vault unlocked:
sed -i -E 's#/run/pidvault/(stored/)?#/run/pidvault/stored/#g' /run/pidvault/stored/*/config
sudo apt install pidvault # once per machine; see Installing
pidvault create # once: asks for a passphrase, unlocks this shell
$EDITOR /run/pidvault/stored/default/config # starts as a commented example
pidvault stub # once: put its output at the top of ~/.ssh/config
pidvault unlock # in each further shell that should have access
create and import unlock the shell they ran in; --no-unlock leaves it locked.
pidvault create --from DIR starts from the files in a directory instead of an empty
vault. Keys you copy in from ~/.ssh have been readable by every process you ran so far:
the clean move is to generate new ones straight into the vault, e.g.
ssh-keygen -t ed25519 -N '' -f /run/pidvault/stored/default/id_ed25519, and retire the old ones.
pidvault stub prints a block that must sit at the top of ~/.ssh/config:
Match exec "/usr/bin/pidvault match --kind stored"
IdentityAgent /run/pidvault-agent.sock
AddKeysToAgent no
Include /run/pidvault/stored/*/config /run/pidvault/ephemeral/*/config
In a shell with a stored vault unlocked, the vaults' entries take precedence and ssh signs
through the vault agent, never handing a key to any other agent whatever a later
AddKeysToAgent yes says. Everywhere else the block does nothing and your existing
config applies as before, so you can migrate one host at a time. ~/.ssh itself,
authorized_keys and inbound SSH are untouched. Shells with a vault unlocked do not use
your desktop agent, so a key you still keep there works only in other shells until you
move it into a vault.
pidvault match decides which shells that is: it succeeds when the shell has an unlocked
vault meeting every condition given, --kind stored|ephemeral, --name NAME and
--with-keys, and with none, any unlocked vault. --kind stored leaves a shell holding
only an ephemeral token vault to your usual agent and config; --with-keys would also
take in ephemeral vaults with SSH certificates; --name work limits the block to one
vault.
If that block is more magic than you want in ~/.ssh/config, leave it out and name the
vault's config yourself: ssh -F /run/pidvault/stored/default/config HOST. A new vault's
starter config already points ssh at the vault agent, so nothing else is needed. -F
replaces ~/.ssh/config and /etc/ssh/ssh_config rather than adding to them; jump hosts
work with ProxyJump, which passes -F on, but not with a ProxyCommand running
ssh, which does not; and other tools need telling too (scp -F,
rsync -e "ssh -F …", git's core.sshCommand).
An unlocked shell, and everything it starts, can change the vault like any directory:
editors, sed -i, ssh-keygen and ssh itself all work. Changes are saved on fsync,
when a written file is closed, on a rename, and otherwise within a second.
Inside the vault, refer to files through the mount, e.g.
IdentityFile /run/pidvault/stored/default/id_ed25519 (with its .pub beside it) and
UserKnownHostsFile /run/pidvault/stored/default/known_hosts. Keep ControlPath elsewhere
($XDG_RUNTIME_DIR): unix sockets cannot live on the mount. A vault holds up to 4 MiB.
pidvault unlock --for 30m makes any unlock end by itself; status shows how long is
left. Once no shell has a vault unlocked, its decrypted contents and key leave memory.
Other commands: pidvault status, pidvault lock (this shell) and pidvault lock --all
(every shell), and pidvault passwd to change a passphrase.
To see which shells have a vault unlocked, put this at the end of ~/.bashrc:
eval "$(pidvault shell-init)"
While a shell has a vault unlocked, its prompt starts with a red marker naming the vault
and the time left, e.g. 🔑 stored/default 9m, and the terminal title starts with the same.
Both go away at the next prompt once the vault is locked or runs out. The marker is a
reminder, not a safeguard: any process of yours can print one. It is for bash; the rest
of your prompt stays as it was.
A file in a vault is a key when it is an unencrypted private key in OpenSSH format, as
ssh-keygen -N '' writes them. pidvault tells by its first bytes, which hold nothing
secret, so a key is protected from the moment it is written:
200 and cannot be opened for reading by anyone. Its bytes cannot be
changed either, only appended to, emptied, renamed or deleted, so nothing can turn it
back into a readable file; a copy is a key too. export (which asks for the
passphrase) is the only way to get it out again.ssh-add) is refused; keys get in by being
written to a vault.rsa crate, whose arithmetic
is not constant-time (RUSTSEC-2023-0071,
unfixed); every RSA signature is blinded against timing, but a process in an unlocked
shell may time as many as it likes, and ed25519 does not have the problem.config, known_hosts, .pub files, and keys
with a passphrase (which the agent cannot use; ssh-keygen -p -N '' -f KEY removes
the passphrase, after which the key is protected). Keys in the old PEM format stay
ordinary too; ssh-keygen -p -f KEY rewrites them in OpenSSH format.To bring in a key you already have, pidvault add-key ~/.ssh/id_github (with --vault
and --name if you like) asks once for its passphrase, if it has one, and writes the key
without it, and its .pub, into the vault. The original is left alone: it has been
readable by every process you ran, so retire it once the copy works.
A key that deserves more protection than the rest belongs in a vault of its own: its own
passphrase, unlocked only in the shell that needs it, and with its own agent.toml.
Unlock it for just as long as you need it, and it locks again by itself even if you
forget:
pidvault unlock signing --for 10m # also 90s, 1h30m; unlocking again sets a new limit
ssh tells the agent which connections it opens on behalf of a host you forward your
agent to. On those, the vault agent offers no key unless the vault's agent.toml allows
it, so remote root cannot sign with your keys even where ForwardAgent yes is set:
# /run/pidvault/stored/default/agent.toml
[keys."id_jump"] # the key's path in the vault
forward = true # may sign through hosts this agent is forwarded to
A policy that cannot be read lets nothing be forwarded, and pidvault status says why.
Older ssh versions do not mark forwarded connections; with those, everything looks local.
pidvault create prod # its own key, and its own passphrase if you like
pidvault unlock prod # in the shell that should have it
$EDITOR /run/pidvault/stored/prod/config
pidvault lock prod # lock just that one
Every command takes a vault name and defaults to default; export and import take
--vault NAME. Vaults are included in alphabetical order and ssh uses the first value it
finds for each setting, so a Host * block in one vault applies to every host in shells
that have it unlocked. Paths inside a vault contain its name: renaming one is not
supported.
For secrets you can get again by logging in, such as tokens, keep nothing on disk:
pidvault create --ephemeral bao --for 8h # no passphrase; only this shell, for 8 hours
export BAO_TOKEN_PATH=/run/pidvault/ephemeral/bao/token
bao login -method=oidc # bao stores its token in the vault
An ephemeral vault starts empty and is held in memory only, under
/run/pidvault/ephemeral, apart from the stored vaults under /run/pidvault/stored. It
belongs to the shell that created it and what that shell starts: no other shell sees it
or can unlock it, since there is no passphrase to prove it is you, and it cannot be
exported. Its name is the shell's own, so every shell can have its own bao at the same
path, and BAO_TOKEN_PATH can be set once in ~/.bashrc; in a shell without one, bao
finds no token. status shows it as ephemeral/bao, and pidvault lock --ephemeral bao
ends it. It is gone when that shell exits or locks it, when --for runs out, or when the
daemon stops. Keys in it sign through the agent like any other, which suits short-lived
SSH certificates; for ssh to use them in a shell without a stored vault, make the stub's
Match exec run pidvault match --with-keys.
A secret in a vault is safe from your other processes only while programs read it from
the vault themselves, as bao does here. A program that hands it on, through a pipe, a
helper program or an environment variable, gives it away: any of your processes can
read another's pipes and environment through /proc.
az keeps its refresh tokens in plain files under ~/.azure, readable by every process
you run. Give it a vault of its own and point its whole configuration directory there:
pidvault create azure
export AZURE_CONFIG_DIR=/run/pidvault/stored/azure # in ~/.bashrc
az config set core.collect_telemetry=false logging.enable_log_file=false
az login
Move the directory, not a symlink to the token cache: az logout deletes the cache file,
and the next az login would then write a new one under ~/.azure. In a shell without
the vault az stops with a PermissionError on that directory and writes nothing elsewhere.
Turning off telemetry and the log files keeps the vault far below its 4 MiB, and
AZURE_EXTENSION_DIR can keep extensions out of it, though anything there runs in a
process that holds your tokens.
Log in afresh rather than copying the old cache in, then delete ~/.azure/msal_token_cache.json
and service_principal_entries.json; az logout revokes nothing, so sign out
everywhere in your Microsoft account to end what they held.
az account get-access-token refreshes an access token with the refresh token, inside
the vault, and prints it. A program that reads it through a pipe hands it to your other
processes for its hour or so of life, as with any pipe; the refresh token stays in the
vault.
docker login keeps registry passwords and tokens in ~/.docker/config.json, base64
but otherwise plain. Move the whole directory into a vault, and keep the builder state,
which is not secret and grows, outside it:
export DOCKER_CONFIG=/run/pidvault/stored/docker # in ~/.bashrc
export BUILDX_CONFIG=~/.docker/buildx
az acr login runs docker login, so its tokens land in the vault too. In a shell
without the vault docker runs without credentials: public images pull as before, and
nothing is written under ~/.docker. Being in the docker group is another matter; see
What it does not do.
pidvault export ~/pidvault-backup.age # from an unlocked shell; asks for the passphrase
pidvault import ~/pidvault-backup.age # on the new machine; asks for its passphrase
An export is a standard age file encrypted with your
passphrase, holding a tar archive of the vault (age -d backup.age | tar -t lists it).
Only an unlocked shell may export, so your other processes cannot even get a copy to try
passphrases against. import checks the passphrase before storing anything. Replacing an
existing vault also needs an unlocked shell. The old one is kept as
<name>.pidvault.previous, where an administrator can restore it, and shells that are
already unlocked see the new contents at once.
pidvault account and never as root: no
capabilities, a read-only system, no /home, no devices, no network and a system-call
allow-list (systemd-analyze security rates it 0.5, "safe"). systemd creates its sockets
in root's /run, and a separate, minimal root service does the one thing that needs
root: it mounts the FUSE filesystem at /run/pidvault, hands the connection to the
daemon, and unmounts as soon as the daemon is gone. Your processes cannot signal the
daemon, trace it, read its memory, unmount the view or place a socket or file where it
lives./var/lib/pidvault/<uid>/<name>.pidvault, readable only by the
daemon. Vaults belong to the uid, as a home directory does: renaming an account keeps
them, and when an account is deleted its /var/lib/pidvault/<uid> should go with its
home directory, or a later account given the same uid will find them listed (though it
cannot open them). It holds a random 256-bit key K encrypted with your passphrase, and the contents
encrypted with K, both in age passphrase format; one file, so a save replaces both in
a single rename. Unlocking costs one scrypt run on your passphrase (cost 2^18, 256 MiB,
above OWASP's recommended minimum), so a stolen copy of the file costs an attacker that
much per guess; new passphrases need at least 15 characters, NIST's minimum for a
password that is the only factor. Saving costs almost nothing, since K needs no slow
key derivation. Because only someone holding K can
produce contents that decrypt with it, nobody without K can plant a vault./proc/self, a vault's path is
the same for everyone: each user gets their own directory there (a separate inode, owned
by that user, mode 0700), listed only to them, and its contents only in a process that
is, or descends from, a shell of that user that unlocked that vault. Vault paths
therefore contain no username, so a vault works unchanged on machines where your account
is named differently. Shells are held by pidfd, so a reused pid inherits nothing.MemorySwapMax=0), and the daemon cannot be core-dumped or traced. Hibernation writes
all memory to disk, this included.
Unlocking another shell while one is open only checks the passphrase and keeps what is
in memory.cargo test # unit and end-to-end tests, no root needed
tests/vm/run.sh # the .deb, systemd and root paths, in a throwaway VM; no sudo
cargo deb # target/debian/pidvault_<version>_amd64.deb
The end-to-end tests need /dev/fuse and fusermount3. They run the daemon with
--unprivileged and set PIDVAULT_INSECURE_ALLOW_SAME_UID_DAEMON, which is exactly the
setup the client otherwise refuses. One test logs in to a throwaway sshd on a random
local port with a key that exists only in the vault.
tests/vm/run.sh needs access to /dev/kvm, qemu-system-x86_64, genisoimage, gpgv
and cargo-deb. It downloads the Ubuntu cloud image once into ~/.cache/pidvault-vm,
checked against Ubuntu's signed SHA256SUMS, and boots two VMs in parallel, each from a
fresh overlay. In one, tests/vm/guest-tests.sh installs the package and tests it as root,
with two users whose processes run under real systemd --user managers, then reinstalls,
removes and purges it. In the other, tests/vm/guest-upgrade.sh installs the latest release
from the published apt repository, puts a vault to use, upgrades to the package under test
and checks that the vault, its keys and the agent still work. It skips the upgrade when
nothing older than the package under test is published. Results come back as TAP on each
VM's second serial port.
fuzz/ holds cargo-fuzz targets for what other processes can hand the daemon: agent
messages, control requests, imported archives and stored vault files, agent.toml, and
any sequence of file operations through the mount (which must leave the vault savable
and never make a key readable). They need a nightly toolchain:
cargo +nightly fuzz run tree_ops, with fuzz/seeds/<target> as an extra corpus
directory where there is one. CI fuzzes each target for two minutes on every change and
for half an hour weekly, keeping the corpus between runs.
packaging/ has the systemd units — the daemon and its sockets, and the mount helper and
its socket — and the sysusers.d entry for the pidvault account.
cargo deny check (configured in deny.toml, whose accepted advisories
osv-scanner.toml repeats for Scorecard, with a copy in fuzz/) checks dependencies against the RustSec
advisories, the allowed licenses and crates.io as the only source; CI runs it, and lintian on
the package. packaging/pidvault.1 and packaging/changelog.Debian are generated: tests
fail when they are stale and say how to regenerate them.
To release, bump the version in Cargo.toml, turn ## Unreleased in CHANGELOG.md into
## X.Y.Z - YYYY-MM-DD, regenerate the man page and Debian changelog, merge, and push a tag
vX.Y.Z on main. The release workflow runs the full test suite and builds the package
twice, in different directories, refusing to go on unless both are identical. The binary
carries its dependency list (cargo auditable), and the package gets a build provenance
and a CycloneDX SBOM attestation. The workflow then waits for approval in the release
environment before it creates the GitHub release, signs the apt repository (built by
scripts/build-apt-repo.sh from every released package) and publishes it on GitHub Pages.
Only that approved job can reach the signing key.
Anyone can check a downloaded package with gh attestation verify pidvault_X.Y.Z-1_amd64.deb --repo torstei/pidvault, adding --bundle with the release's .sigstore.json to check it
without asking GitHub, or rebuild it from the tag and compare: with the Rust version
named in the release notes, SOURCE_DATE_EPOCH set to the tagged commit's time
(git log -1 --format=%ct), cargo auditable build --release --locked and
cargo deb --no-build.
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
unlock registers the shell that ran it, and only if that shell is in the same session:
a client orphaned with (pidvault unlock &) is adopted by systemd --user, which must
not end up unlocking the whole login.