Pass non git-tracked data to flake

I’m trying to setup build process for my project. In one step I need key file, I would like to keep somehow secret. “Traditional” way I tried, was to simply .gitignore it and get to the source directory differently. This does, not work as, with flakes nix, will not see it.

I stumbled upon sops. Using it via sops-nix failed, as it seems to be for NixOS, and using sops inside derivations have problems with sandboxing. Later one was not able to access GPG key to decrypt secret.

At this point I ended up with following, which is not the best.

signing-key = pkgs.requireFile {
  name = "key.pem";
  message = ''
    Android signing key: key.pem is not loaded.
    Load it with:
    > nix-store --add-fixed sha256 key.pem
  '';
  hash = "sha256-aW8rpNcy7aIOTdaS3wl5F+bSaEbKYAvT4mycPtHX260=";
};

I wonder if there is some better way, to pass such “non public” dependency to the flake?

I personally like to use a flake as a function (the function takes the secrets as input and return whatever data you want). The flake you have on the public git repo have something like that:

{
  # inputs, ...
  output = { nixpkgs, ... }:
    {
      fromSecrets = secrets:
        # actual code here
    };
}

and then you create another private flake that imprts the public flake and calls public-flake.fromSecrets {token = "secret";} to get the result.
This way the secrets are stored in your private flake, and a function that takes the secrets as input is stored publicly. (btw you can use path:/idk/public-flake/ instead of github:... to import a local flake in the inputs)
Idk if it’s really clear, I hope this helps

It’s impossible for the eval/build process of a flake to depend on any data that isn’t copied world-readable into the nix store. This is intentional, for determinism/reproducibility purposes. There is no way around this except --impure or just not using a flake.

Sops-nix and agenix get around this by making the encrypted form of a secret the only thing that goes through the eval/build process, then decrypting the secret with an external key at runtime. It’s only really an applicable approach for activated systems like nixos/home-manager/nix-darwin.

Using a non-git local flake input does not get around the world-readable copy in the nix store, but can get around the issue of the key being available publicly if you put your main flake on github or something. Depending on what you’re worried about, it might be a viable option.

2 Likes

Here’s what I would do. First, make sure that the Git repository contains at least three files: flake.nix, flake.lock and missing-secret.txt. Here’s what the flake.nix file would look like:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
    secretFile = {
      url = "path:./missing-secret.txt";
      flake = false;
    };
  };
  outputs =
    {
      self,
      nixpkgs,
      secretFile,
    }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages."${system}";
    in
    {
      packages."${system}".default = pkgs.writeShellScriptBin "example-script" ''
        echo Here’s the contents of secretFile:
        printf '%s' ${nixpkgs.lib.strings.escapeShellArg (nixpkgs.lib.strings.readFile secretFile)}
      '';
    };
}

Here’s what the missing-secret.txt file would look like:

Oops! You forgot to override the secretFile input.

At this point, if you run nix run, then the flake will output the following:

$ nix run
Here’s the contents of secretFile:
Oops! You forgot to override the secretFile input.

You would then create a separate file that isn’t tracked by Git and use --override-input to tell your flake to use it like this:

$ nix run --override-input secretFile path:./secret-file.txt
warning: not writing modified lock file of flake 'git+file:///home/jayman/Documents/Home/VC/Git/My%20repos/flake-that-can-access-secret':
• Updated input 'secretFile':
    'path:./missing-secret.txt'
  → 'path:/home/jayman/Documents/Home/VC/Git/My%20repos/flake-that-can-access-secret/secret-file.txt?lastModified=1759701284&narHash=sha256-h4CSwqmIJD59Dih6f/1hvRX4StuQQ9yJCa%2B0ufjcyZs%3D' (2025-10-05)
Here’s the contents of secretFile:
This is my example secret.

@tejing does bring up a good point, though. If you use this approach, then the secret will be copied to the Nix store and will be world readable. If you use remote builders or something like nixos-rebuild --target-host, the secret might even be copied to the Nix store on multiple different machines.

1 Like

To actually sign your binary, just do that outside of the context of a nix derivation. Use nix to build the artifact, take it out and sign it in a separate flow.

You can even use nix to “do” that, and use a flake to encode this, with a script in outputs.apps and nix run.

Something like:

# flake.nix
{
  outputs = { self, nixpkgs, ... }: {
    apps.x86_64-linux.sign-apk = let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
      app = self.packages.<arch>.apk;
    in {
      type = "app";

      program = (pkgs.writeShellScript "sign-apk" ''
        PATH="$PATH:${pkgs.lib.makeBinPath [ 
          # Insert packages that provide keytool and apksigner here
        ]}"

        # In practice, probably use a different tmpdir, since `/tmp` is not a tmpfs
        # and has non-user permissions - `XDG_RUNTIME_DIR` is a good alternative       
        mkdir -p /tmp/app-signing
        cp ${app}/my-app.apk /tmp/app-signing/
        keytool -importcert -file /some/path/tokey.pem -keystore /tmp/app-signing/keystore.jks -alias app-key 
        apksigner sign --ks-key-alias app-key --ks /tmp/app-signing/keystore.jks /tmp/app-signing/my-app.apk
        cp /tmp/app-signing/my-app.apk . 
      '').outPath;
    };
  };
}

Now nix run .#sign-apk produces a signed apk in the current directory.

2 Likes

I managed to came up with sort of solution.

Apparently when sops are called from builtins.exec they have access to GPG environment, and is able to decrypt secrets.

secrets = {
  signing-key = builtins.exec [
    "${pkgs.runtimeShell}"
    "-c"
    "echo \\'\\' && ${pkgs.sops}/bin/sops decrypt ${./.secrets/signing-key} && echo \\'\\'"
  ];
};

This brings couple more issues: I hadn’t find easy good/simple way to escape output, and I’m currently wrapping it the ugly way. Additionally exec require allow-unsafe-native-code-during-evaluation to be enabled, but this for some reason does not work via flake nixConfig, so it either have to be passed via cmdline or nix.conf. Not a huge problem, but anoying.

I will repeat, evaluating that string (before even executing any build) exposes your secret.

This is a terrible hack.

So, a few things about the last iteration. You’re introduced impurity and also security risks. The result of that derivation will be your clear text secrets written into the nix store. Additionally, it probably won’t reproduce if there’s deviances in the environment.

However, you can get around that if you eschew eval time secrets (which always put the clear text secrets that are world readable in your store). How you slice that up depends on where in the flow you need your signature. The user’s GPG keys are overall not going to be friendly when used with a nix sandbox build env.

This is pretty ugly. I very much recommend TLATER’s idea above over this. Relying on an off-by-default (for very good reason) nix option to allow you to break the purity of flakes without actually specifying --impure (arguably a bug. It should really require both.) is pretty awful.

builtins.exec is undocumented and I’m unfamiliar with it, but I strongly suspect you still end up copying the signing key to the nix store world-readable due to the output of the exec being in the nix store.

Again, I strongly recommend TLATER’s idea above over this.

1 Like
  nixConfig = {
    sandbox-paths = [
      "/some/patch=/to/map"
    ];
  };

is the best I managed to achieve. It either can be used to pass secrets directly, without being saved /nix/store, or for example to pass the sops socket. The later one seems to be working nice.

Still exploring the topic, thou.