Skip to content

Latest commit

 

History

History
309 lines (258 loc) · 13.6 KB

README.md

File metadata and controls

309 lines (258 loc) · 13.6 KB

nodkey

An AI agent asks. You approve on your phone. The secret goes to one command, not to the agent.

  • The agent writes a command, for example nodkey run --secret GH_TOKEN=github-token -- gh release create v1.2.0.
  • You see the request on Telegram and tap Approve.
  • nodkey puts the value only into the environment of that command. It never puts it into the agent's chat. The agent sees the command's output and nodkey's own status lines, never the value from nodkey.
  • SSH keys stay on the broker. The client gets only signatures.
  • The limits: the agent chooses the command. A bad agent can write one that prints or sends the secret, and an approved SSH login is a shell. So read the command before you tap. And the command runs as the agent's own user, so while it runs, a bad agent can also read the value from the command's environment (/proc/PID/environ). So approve only a secret that you accept this agent may get, and prefer short-lived or narrow secrets.

The agent writes the command. The nodkey client asks the broker. The broker asks you on your phone. After you approve, the broker sends the value only into the command's environment. nodkey gives the agent only the command's output. For SSH, the broker sends only a signature; the key stays on the broker.

Warning

nodkey is work in progress. It is new, it has had no outside security audit, and it may have bugs that leak a secret. Do not use it as the only thing that keeps a secret safe.

nodkey does not protect you from a malicious agent once you approve. It makes the agent ask before each use, and it keeps the keys off the agent's machine. But the agent writes the command, so once you approve a request, it can make that command print, save or send the secret, or read it while the command runs (see the limits above), or use that SSH login for other things (it is a shell). A tap helps only if you read the request well, and even then the command and the reason are only what the agent claims. Some ways to reach a secret skip nodkey: root on the broker host, a login to the secret store on the agent machine, or your Telegram account logged in there. The full list of limits is in THREAT-MODEL.md section 5.

nodkey comes with no warranty, as the license says. You use it at your own risk. We are not responsible for a secret that leaks or is lost, or for what an agent does with it.

Status: work in progress. On 2026-10-15, the code of version 1 is on main, and the test with a real Telegram bot (#17) is done. The latest release is on the release page. Try it on a test setup first. See the v1 milestone.

How it works

  • A small daemon, the broker, runs on a trusted machine. It holds the SSH keys and the access to your secret stores (Bitwarden Secrets Manager through bws, the Bitwarden Password Manager through bw, a vault of 1Password, or a project of Infisical).
  • The agent's machine gets only the nodkey client. The agent never gets an SSH key or a store token. The client's own TLS key lets it ask, not approve.
  • The agent writes this command, and nodkey runs it with the secret added: nodkey run --secret GH_TOKEN=github-token -- gh release create v1.2.0. The value goes only into that one command's environment. nodkey never shows the value to the agent; the agent sees the command's output. nodkey never prints the value, but the command itself can: the agent chose the command, so check it before you tap.
  • When ssh, git or Ansible need a signature, nodkey ssh-agent sends the request to the broker. The key stays on the broker; the client gets only the signature.
  • When a request needs approval, you get a Telegram message with Approve once and Deny, and for some secrets and keys also Approve N min. The broker checks which machine asks, for which secret or key, and for SSH the real target host; the message shows these as verified. The command and the reason in the message are only what the agent claims. No answer means no. In place of Telegram, the broker can use an ntfy server that you run yourself (docs/setup.md).
  • Every request and decision goes into a hash-chained audit log.

Try it

See the flow first, on one Linux machine, in a few minutes. nodkey try needs no root, no second host, no Telegram bot, no store account and no config file. It uses three fake secrets, and asks in its own terminal in place of your phone.

With nodkey installed (step 1 of the quick start), run in one terminal:

nodkey try

In a second terminal, run the command that it prints:

nodkey try run --secret DEMO_TOKEN=demo-token -- sh -c 'echo "got ${#DEMO_TOKEN} characters"'

The second terminal plays the agent's screen. The request shows up in the first terminal. Type 1 (Approve once), 2 (Approve 5 min) or 3 (Deny), then Enter. Or wait 2 minutes: the request expires and is denied. While the request waits, the second terminal shows its short code. After you approve, it shows got N characters, never the value.

Now see the limit. Run a command that prints the value:

nodkey try run --secret DEMO_TOKEN=demo-token -- sh -c 'echo "$DEMO_TOKEN"'

Approve it, and the fake value shows up in the second terminal. This is the limit: the command can print the secret, and the agent writes the command. So you read the command before you tap.

Press Ctrl-C in the first terminal to end the trial; it removes its files.

Try mode is not a setup. The agent could type the answer in that terminal, so the trial has only fake values (they start with TRY-) and can never reach a real secret. A real setup adds a separate broker host, a secret store, Telegram on your phone and the rules of each secret: see below and docs/setup.md.

Quick start

This is the short way, for a first test with one secret. The full setup, with the safety steps that real use needs, is in docs/setup.md.

where what it gets
the broker host: a trusted Linux machine with systemd, always on, with no agent on it nodkey, which runs nodkey broker; bws; the bot token, the store token and the TLS key of the broker, as encrypted systemd credentials
the agent machine: where the agent works nodkey, and its own TLS key
your phone Telegram, with a bot of your own

Do the setup of the broker host on its console, or from a trusted computer where no agent runs. Make and copy the tokens only there or on your phone, never through the agent machine: the agent may read its clipboard, terminals and browser. Keep no SSH key or other way to get root on the broker host on the agent machine (docs/setup.md section 1).

  1. Install nodkey on both machines, as root on each machine. Never download, build or copy it as the user of the agent. Get the root shell in a way that does not go through the agent's account: on the broker host, its console or a trusted computer; on the agent machine, its console or SSH as an admin user from a trusted computer, not sudo in the terminal where the agent works. (nodkey's promises come from the broker, which trusts nothing from the client: a changed client gets no more than what you approve on the phone.)

    The easy way is the installer. It installs the newest release as /usr/local/bin/nodkey. First it checks the SHA-256 and the build attestation of the release, and the version of the new file. Download it as root, read it, and run it as root. In a root shell that you got as above:

    curl --proto '=https' --tlsv1.2 -fsSL -o /root/install.sh https://raw.githubusercontent.com/code6x/nodkey/main/install.sh
    less /root/install.sh
    sh /root/install.sh

    The installer needs the Sigstore bundle of the attestation in the release, and 0.1.0 has none. For 0.1.0, get nodkey by hand on a trusted machine, check it, and copy it to both machines, as docs/setup.md section 2 says. Later, nodkey update, as root, installs a new release.

  2. Make a Telegram bot with @BotFather (/newbot), on your phone or a trusted computer. Open the bot and tap Start, and find your Telegram user ID (docs/setup.md section 4). Never log in to this Telegram account on a machine where an agent runs: whoever can use the account can approve. Put the secret into Bitwarden Secrets Manager, and make a machine account with an access token that can read it. Do this with a Bitwarden account that is never logged in on a machine where an agent runs: whoever can use the account can read the secret without nodkey. Install bws as /usr/local/bin/bws, owned by root (section 5).

  3. On the broker host, as root, make the user of the broker, its TLS key and the encrypted credentials. Use --with-key=host instead of --with-key=host+tpm2 if systemd-creds has-tpm2 does not say yes.

    useradd --system --home-dir /var/lib/nodkey --shell /usr/sbin/nologin nodkey
    install -d -m 0755 /etc/nodkey
    install -d -m 0700 /etc/nodkey/credentials /run/nodkey-setup
    # prints the pin of the broker
    nodkey keygen --out /run/nodkey-setup/broker-tls-key
    systemd-creds encrypt --with-key=host+tpm2 --name=broker-tls-key \
      /run/nodkey-setup/broker-tls-key /etc/nodkey/credentials/broker-tls-key.cred
    rm /run/nodkey-setup/broker-tls-key
    systemd-ask-password -n "Telegram bot token:" |
      systemd-creds encrypt --with-key=host+tpm2 --name=telegram-token - /etc/nodkey/credentials/telegram-token.cred
    systemd-ask-password -n "bws access token:" |
      systemd-creds encrypt --with-key=host+tpm2 --name=bws-token - /etc/nodkey/credentials/bws-token.cred
    # empty: no SSH keys yet
    install -m 0644 /dev/null /etc/nodkey/known_hosts
  4. On the agent machine, make the TLS key of the client:

    install -d -m 0700 ~/.config/nodkey
    # prints the pin of the client
    nodkey keygen --out ~/.config/nodkey/client.key

    Then write ~/.config/nodkey/client.toml, with mode 0600:

    broker = "192.0.2.10:7743"                # the broker host on your private network
    broker_pin = "sha256:Q2b9...base64..."    # the pin of the broker
    tls_key = "~/.config/nodkey/client.key"
  5. On the broker host, write /etc/nodkey/broker.toml, owned by root with mode 0644:

    listen = "192.0.2.10:7743"
    tls_key = "${CREDENTIALS_DIRECTORY}/broker-tls-key"
    audit_log = "/var/lib/nodkey/audit.jsonl"
    known_hosts = "/etc/nodkey/known_hosts"
    
    [telegram]
    token_file = "${CREDENTIALS_DIRECTORY}/telegram-token"
    user_id = 123456789                       # your Telegram user ID
    
    [client.laptop]
    pin = "sha256:Yf3c...base64..."           # the pin of the client
    
    [store.main]
    type = "bws"
    program = "/usr/local/bin/bws"
    token_file = "${CREDENTIALS_DIRECTORY}/bws-token"
    
    [secret.github-token]
    store = "main"
    id = "00000000-0000-0000-0000-000000000000"   # the ID of the secret in Secrets Manager
    clients = ["laptop"]
    ask = "always"

    Take the example unit from the tag of the version you installed (contrib/systemd/nodkey.service at the tag vX.Y.Z), never through the agent machine (docs/setup.md section 8). After a build from the source, use the unit in your copy of the repository instead. For a release, on the broker host:

    VERSION=$(/usr/local/bin/nodkey version | cut -d' ' -f2)
    curl -fL -o /etc/systemd/system/nodkey.service "https://raw.githubusercontent.com/code6x/nodkey/refs/tags/v$VERSION/contrib/systemd/nodkey.service"

    Keep only the LoadCredentialEncrypted= lines of broker-tls-key, telegram-token and bws-token. Then start the broker, and read its log:

    systemctl daemon-reload
    systemctl enable --now nodkey
    journalctl -u nodkey
  6. The first run, on the agent machine:

    nodkey run --secret GH_TOKEN=github-token --reason "test the setup" -- \
      sh -c 'echo "the command got ${#GH_TOKEN} characters"'

    A Telegram message comes, with the same short code that the client prints. Tap Approve once. The command gets the secret in GH_TOKEN, and prints only its length, so the agent's screen shows only that length.

Before real use, read docs/setup.md: above all the safety of your Telegram and Bitwarden accounts and of the broker host (section 9), SSH logins (section 11), and the check of the audit log (section 12).

Read more

License

Apache-2.0