Introduction to Yeet
Yeet is an PULL-based deployment server for Nix closures. Yeet only acts as an intermediary and does leave you open to choose your own nix cache and build-systems.
The indent of Yeet is to:
- Provide an easy way to manage your whole fleet of devices - from homelab to enterprise
- Not pose any restrictions onto how your nix derivation is built
- Allow for clients to be offline when the updates is created
- Secure defaults to protect your fleet
Why do I want Yeet?
Wheter you are a person who manages their homelab server or an business which manages a large fleet of devices. Installing, configuring and maintaing your devices imperatively will bite you back in the long run.
Push based deployment system do work really well if you only deploy to servers. Once you also want to provision devices that may not be online this gets a whole more complicated. Now you have devices that miss updates, fly under the radar and clog up your pipelines.
This is where Yeet comes into play. By reversing the role with an on-device agent, the clients now signal to the server that they are ready to receiver their update.
This allows to offload the heavy lifting of the build process to build-machines. Especially in infrastractures that include hundreds of devices the savings get noticeable.
Architecture
architecture-beta
group clients[Fleet]
service yeet-server(game-icons:catapult)[Yeet Server]
service laptop(mdi:laptop)[Laptop] in clients
service desktop(mdi:desktop-tower)[Desktop] in clients
service server(mdi:server)[Server] in clients
service cache(devicon:nixos)[Nix Cache]
service source(logos:git-icon)[Fleet Definition]
service build-machine(mdi:build)[Build Machine]
service pipeline(logos:github-actions)[Build Pipeline]
junction tl
junction tlb
junction tr
junction trb
source:B -- > T:pipeline
pipeline:L -- R:tl
build-machine:R -- R:tr
pipeline:R --> L:build-machine
tr:B -- T:trb
tl:B -- T:tlb
tlb:B --> T:yeet-server
trb:B --> T:cache
junction y_top
y_top:B -- T:y_middle
junction y_middle
y_middle:B -- T:y_bottom
junction y_bottom
y_middle:L --> R:yeet-server
laptop:L <-- R:y_top
desktop:L <-- R:y_middle
server:L <-- R:y_bottom
junction c_top
c_top:B -- T:c_middle
junction c_middle
c_middle:B -- T:c_bottom
junction c_bottom
c_middle:R -- L:cache
laptop:R <-- L:c_top
desktop:R <-- L:c_middle
server:R <-- L:c_bottom
Quickstart
nix flake init --template github:srylax/yeet
Go to https://app.cachix.org/cache. Register if you have no account.
Create a new binary cache - note the name you are going to need it later.
Create a new authtoken on cachix.
cachix authtoken <token>
yeet-server
You check out your current build with yeet vm my-nixos (only for nixos not for darwin)
yeet publish --cachix <cache> -> client are now listed as unverified
Get the code from your vm via
yeet approve aegis <code> -> Client is now listed as verified
Edit the file my-nixos/configuration.nix and add ripgrep:
yeet publish --cachix <cache> -> client are now listed as unverified
Client automatically get the update
yeet log mynixos [TODO]
Endpoints
The Lifecycle of an client is as following:
stateDiagram-v2
state if_state <<choice>>
[*] --> New
New --> Standby: Register
Standby --> Updating: Update Available
Updating --> if_state
if_state --> Update: Successful
if_state --> Rollback: Failure
Rollback --> Standby: Mark update as Failure
Update --> Standby: Update remote version
Varlink
Varlink allows the user facing cli to perform actions that would otherwise require configuration or authentication. The idea is that even if the user is not an admin, if the yeet server allows it, the user can perform system modifying action by leveragin the running yeet agent. Furthermore if the user wants to execute commands on the yeet server, instead of the cli executing the command itself it will ask the daemon to do it (TBD)
This is different for yeet server commands as these are for non interactive use or for debugging.
Security
The yeet-daemon will at startup create a socket at /run/yeet/agent.varlink. This socket is only read+write for the yeet group.
Even for basic status queries the user has to be in the yeet group.
TBD: If the user whishes to execute a varlink method that requires more permissions e.g. detach the daemon, a polkit request is created to authenticate the user. This allows administrators to fine-tune the actions a user can take on the yeet daemon.
An example configuration includes the user always in the yeet group (to allow the status query) but disallows any higher functions.
Design
The varlink thread is not coupled with the agent thread at all. There is no communication that exists between the different thread. Meaning that all information the daemon requires, besides configuration, he has to get by himself. This includes querying the yeet server for information about its state. Because the whole yeet agent does not contain any state this will not inconsistencies. But there is still room for race conditions: If the user performs a modifying action like switching to a new system while the agent thread is currently switching systems themself this can lead to race conditions between the two threads (To be verified). A simple fix is to implement a simple Thread Lock when any modifying actions are executed (TBD).
Detach aka Taskforce mode (TBD)
Sometimes we want to allow to switch to a new system locally. This mean ignoring the version that is provided by the server and instead using a locally supplied one.
This process is called Detaching. Once a client decides to start using the server supplied version again the client gets re-Attached. Detaching is usefull in multiple different scenarios:
- Testing a configuration before rolling configuration
- Installing time critical services (e.g. if you are in an organization where modifying the system is not the norm)
Design
Because the daemon does by design not store any of its state this also includes the current state of attachment. Meaning that if the agent wants to detach he has to consult the server first. This parts also allows for custom policy and rules to apply because the server can refuse the detachment.
Once the server allowed the detachment the server is always replying with the AgentAction::Detach this signals the agent to allow any detach requests.
The agent has to never store information about its state because the server will tell him. AgentAction::Detach only changes the actions taken by the Varlink service.
The agent part of the daemon still continues to report the current system in the specified time interval, providing the server with information about the current closure.
Local only detach
There may be times were no connection to the yeet server is possible or event wanted. For this event the --force option exists, signaling the daemon to not contact the server and instead force-switch to a new derivation.
The implication is, that once the daemon regains connection to the yeet server he will automatically switch to provisioned state and use the server provided version, rolling back any changes made by the force switch.
Server
The server should still save updates for a detached client. Once the agent attaches he should get the latest version instead of the version when he detached.
Yeet secret management
GOAL
As a NixOS admin I want to be able to include nix secrets in my os configuration. It is not advised to include the secrets directly in the nix config because this will publicly expose the secrets in the nix-store.
Existing solutions
Widely used solutions to this problems are tools like agenix or sops-nix. These tools encrypt the secret with the public key of the target and then decrypt them once the system is enrolled on the target system with the sshd identity key. This ensures that only the intended host can decrypt the secrets.
This solution has a relative high initial configuration cost and also has the chicken-egg problem: Before we can encrypt a secret we have to get its public key but before we can get the system we have to setup the system.
Furthermore, if we have n hosts, s secrets and a hosts we have to encrypt the secret with (n*s*a) keys. Because administrators will have to be able to decrypt and re-encrypt the secret once a new host needs access to the secret.
These tools require a lot of manual work, it is possible to automate this workflow.
Morph
Morph is another nix deployment tool, it implements secret managing. Morphs secret management is realtively stupid. It simply copies file to the target. See https://github.com/DBCDK/morph/blob/master/examples/secrets.nix. This make it trivial for new users who do not have to manage keys. The chicken-egg problem is also resolved.
Proposed Solution
In Yeet we can make use of the best of both worlds. Yeet has information of the public keys of all hosts. This make it possible for the nix server to manage secrets in a secure way. The flow is best described by a quick example:
yeet secret add nix-cache-signing-key --from-file ../secrets/very-secret.txt
Sequence:
- Yeet cli downloads the public server key
- Content from
../secrets/very-secret.txtis age encrypted with public server key - The Yeet server creates a new secret with the name
nix-cache-signing-keythat is stored at rest encrypted
The admin them would reference this secret in the configuration something like this (pseudo syntax):
yeet.secrets."nix-cache-signing-key" = {
path = "/var/secrets/very-secret.txt";
mode = "0400";
owner = "nginx";
group = "nginx";
};
services.mysuperservices.settings.signingKeyFile = config.yeet.secrets."nix-cache-signing-key".path;
When building the nix derivation at build time, all yeet.secrets are stored in the output derivation.
To support secrets the yeet-agent would have to make changes to the update process:
- Agent receives
SwitchTo->RemoteStoreRemoteStore would now include a fieldfetch_secrets:Vec<String> - Agent requests all secrets from
fetch_secrets(this only includes netrc at the moment) - Agent creates temporary netrc
- Agent fetches the
RemoteStorePath - Agent reads all the secrets that are required by the
RemoteStorePath - Agent tries to request all required secrets (Activation fails if not all secrets exist on the Yeet server)
- Secrets are created by the Agent.
- System Activation begins
- If System activation is successfull delete old generation of secrets else we delete the new generation
On the server the following happens:
- Server receives an
Updatefor a host with somefetch_secretsset - Server receives agent
SystemCheckresponds withRemoteStorePath - Server receives secret request for
netrcsecret.- Check access for
$HOSTfornetrc - Encrypt
netrcwith pub key of$HOSTand respond
- Check access for
- Server receives request for multiple secrets
- Check access for all request secrets
- Encrypt all secrets with pub key of
$HOSTand respond
Advantages
A further advantage is that we can use ephemeral age keys used only for this single transaction.
Alternatives considered
Using agenix with yeet
An alternative would be that when building the derivation yeet could fetch the secrets at build time and age encrypt them with agenix. The main point against this is that yeet would just be an agenix wrapper and create dependencies at build time. With the proposed solution, secrets would be able to change without re-deploying the derivation. Which is a double edged sword because on the one side now derivations are no longer hermetic but on the other side secrets are not meant to be hermetic, they always rely on some state bound secret for unlocking even in the case of agenix.
Future extension
This proposal does not exactly define how the secrets are created on the host. This is because there are many possible solutions:
- create a tmpfs so that they are destroy if powerloss happens
- bind them to the tpm
- plain write them to disk (simplest)
Especially the tpm solution can be interesting for critical secrets as this would protect a device against hardware attacks. Once a device is stolen you can revoke the access on the yeet server and then if the device is tampered with it will lose its secrets and will not be able to fetch them again from the yeet-server.
osquery
Intro to osquery
Osquery (https://osquery.io/) is a tool that allows to query information about an device in an relational-data model way. Osquery is however only the tool / daemon that runs on the device. Extraction of this information is possible in multiple ways:
- logs forwarder to e.g. splunk
- osquery extension
- osquery remote api (tls extension)
The logs forwarding option the the most secure one. With the huge downside that you are not able to run distributed queries and have to make an config deployment each time you want to change a scheduled query.
If you want to use osquery as an investigation tool / information collection in an interactive way you either have to develop a new extension or use the remote api. To orchestrate all the nodes (hosts) you need a central server which implements the remote api. This design document describes this remote api implementation in yeet. To see possible alternative implementations see Alternative remote api implementations
Possible use-cases
The integration of osquery into yeet opens many possibilities for yeet:
- Displaying more information about a host (in yeet)
- Investigate a host with
yeet query - Create warnings on queries e.g. if space is running out | as alternative to e.g. checkmk
The biggest improvement will however be for your specififc use-case. The osquery information can be forwarded to your SIEM and then used to enrich your detection / alerts:
- “real”-time sofware inventory
- integrate osquery data into checkmk
- CVE scanning on installed packages
- Windows Registry tests
- Windows GPO tests
Alternative remote api implementations
Note: this is at the time of writing (03.2026). This is in no way a representative sample nor the opinion of yeet. This is a highly subjective view and is not meant derogatory.
TrendAI integration
TrendAI has a basic integration altough it is hard to use in the UI and currently 14! versions out of date. There is no easy
osctrl
Good initial idea and also has gained some more traction recently but is still very WIP. Deployment is a nightmare and not good documented.
Wazuh
Untested Documentation could be improved. If you already use Wazuh definitely go with it. If not this may be a a big dependency to use for osquery. Like shooting on pigeons with canons.
Elastic Security
Untested Seems to be a very good integration. If you already have Elastic go for it. Unfortunately, not open source.
Goals
- Implement the osquery remote deployment https://osquery.readthedocs.io/en/latest/deployment/remote/
- Be able to run yeet only as a osquery remote
- Run distributed osquery
- Support file carving
- Support scheduled queries
Integration into yeet
All Api request / responses are implemented in an independant osquery-rs crate to allow other crates to easy implement the api.
Permissions
API Design
The following apis map to the osquery flags:
enroll_tls_endpoint = "/osquery/enroll";
config_tls_endpoint = "/osquery/config";
logger_tls_endpoint = "/osquery/log";
distributed_tls_read_endpoint = "/osquery/query/read";
distributed_tls_write_endpoint = "/osquery/query/write";
carver_start_endpoint = "/osquery/carver/init";
carver_continue_endpoint = "/osquery/carver/block";
Enrollment
The osquery client provides an enroll secret set with enroll_secret_path.
Yeet expects the client to supply the content defined in the yeet secret called osquery-enroll.
This secret has to be create via yeet secret add. The content is completely arbitrary.
The node is free to use any host_identifier as long as it is UNIQUE for all nodes.
The response from the server responds with an UUIDv4 node_key.
Client Configuration
Future Work
- integrate an cve scanning into yeet so that you can find cves in osquery
- splunk forwarder
- osquery extension