add sops nix for secret management

This commit is contained in:
Rasyidan Akbar F. 2025-09-30 17:57:13 +07:00
commit 2ea7f853e4
5 changed files with 207 additions and 1 deletions

View file

@ -18,12 +18,15 @@
nix-ai-tools.url = "github:numtide/nix-ai-tools"; nix-ai-tools.url = "github:numtide/nix-ai-tools";
nix-ai-tools.inputs.nixpkgs.follows = "nixpkgs"; nix-ai-tools.inputs.nixpkgs.follows = "nixpkgs";
sops-nix.url = "github:Mic92/sops-nix";
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
zig-overlay.url = "github:mitchellh/zig-overlay"; zig-overlay.url = "github:mitchellh/zig-overlay";
zig-overlay.inputs.nixpkgs.follows = "nixpkgs"; zig-overlay.inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = outputs =
inputs@{ self, nixpkgs, darwin, home-manager, ghostty, nix-ai-tools, ... }: inputs@{ self, nixpkgs, darwin, home-manager, ghostty, nix-ai-tools, sops-nix, ... }:
let let
inherit (nixpkgs.lib) genAttrs; inherit (nixpkgs.lib) genAttrs;
lib = nixpkgs.lib; lib = nixpkgs.lib;

View file

@ -0,0 +1,70 @@
{ config, lib, inputs, ... }:
let
inherit (lib) mkEnableOption mkOption mkIf types;
cfg = config.rsydn.secrets;
sanitizeSecret = name: secret:
let
sopsFile = secret.sopsFile or cfg.defaultSopsFile;
extra = lib.filterAttrs (k: _: !(builtins.elem k [ "path" "mode" "sopsFile" ])) secret;
in
{
path = secret.path or "${config.xdg.configHome}/secrets/${name}";
mode = secret.mode or "0400";
}
// (lib.optionalAttrs (sopsFile != null) { inherit sopsFile; })
// extra;
in {
imports = [ inputs.sops-nix.homeManagerModules.sops ];
options.rsydn.secrets = {
enable = mkEnableOption "sops-nix integration for managing decrypted secrets";
ageKeyFile = mkOption {
type = types.str;
default = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
description = "Path to the Age key used for decrypting secrets.";
};
defaultSopsFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Optional default SOPS file used when a secret definition omits `sopsFile`.";
};
secrets = mkOption {
type = types.attrsOf types.attrs;
default = { };
description = "Secret entries forwarded to `sops.secrets` with sensible defaults.";
example = lib.literalExpression ''
{
"api-key" = {
sopsFile = ./secrets.yaml;
path = "${config.xdg.configHome}/secrets/api-key";
mode = "0400";
};
}
'';
};
};
config = mkIf cfg.enable {
sops =
{
age = {
keyFile = cfg.ageKeyFile;
generateKey = true;
};
secrets = lib.mapAttrs sanitizeSecret cfg.secrets;
}
// (lib.optionalAttrs (cfg.defaultSopsFile != null) {
defaultSopsFile = cfg.defaultSopsFile;
});
home.activation.ensureSecretDir = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
mkdir -p "${config.xdg.configHome}/secrets"
chmod 700 "${config.xdg.configHome}/secrets"
'';
};
}

View file

@ -0,0 +1,70 @@
{
"$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
"blocks": [
{
"alignment": "right",
"segments": [
{
"background": "#003543",
"foreground": "#ffffff",
"style": "plain",
"template": " \ue641 {{ .CurrentDate | date .Format }} ",
"type": "time"
},
{
"background": "#83769c",
"foreground": "#ffffff",
"properties": {
"always_enabled": true
},
"style": "plain",
"template": " \ueba2 {{ .FormattedMs }} ",
"type": "executiontime"
}
],
"type": "rprompt"
},
{
"alignment": "left",
"segments": [
{
"background": "#61AFEF",
"foreground": "#ffffff",
"properties": {
"display_host": false
},
"style": "diamond",
"template": "{{if .Root}} \uf0e7 {{.UserName}} {{else}} {{.UserName}} {{end}}",
"trailing_diamond": "\ue0b0",
"type": "session"
},
{
"background": "#C678DD",
"foreground": "#ffffff",
"powerline_symbol": "\ue0b0",
"properties": {
"folder_icon": "\uf115",
"folder_separator_icon": " \ue0b1 ",
"max_depth": 2,
"style": "agnoster_short"
},
"style": "powerline",
"template": " {{ .Path }} ",
"type": "path"
},
{
"background": "#95ffa4",
"foreground": "#193549",
"powerline_symbol": "\ue0b0",
"style": "powerline",
"template": " {{ .HEAD }} ",
"type": "git"
}
],
"type": "prompt"
}
],
"console_title_template": "{{if .Root}} \u26a1 {{end}}{{.UserName}} \u2794 📁{{.Folder}}",
"final_space": true,
"version": 3
}

5
secrets/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
# Ignore decrypted or temporary secrets material; commit only encrypted SOPS files
*
!.gitignore
!README.md
!*.sops.yaml

58
secrets/README.md Normal file
View file

@ -0,0 +1,58 @@
# Secrets with sops-nix
This repo wires [`sops-nix`](https://github.com/Mic92/sops-nix) into the Home Manager profile so secrets are decrypted on-demand into `~/.config/secrets`. The module is enabled by default and auto-generates an Age key if one does not already exist. Only the *encrypted* payloads under `secrets/*.sops.yaml` are meant to live in git; decrypted files never leave your machine.
## One-time setup
1. Ensure `age` and `sops` are available (e.g. `nix develop` or `nix profile install nixpkgs#age nixpkgs#sops`).
2. Generate or import an Age key. Either let Home Manager create one automatically on first activation or run `age-keygen -o ~/.config/sops/age/keys.txt` yourself.
3. Capture the public half with `age-keygen -y -f ~/.config/sops/age/keys.txt` and add it to the `recipients` list in each encrypted file (`age1…`). Commit the public key under version control or share it through your password manager so other hosts can decrypt.
4. (Optional) Store the private key securely in macOS Keychain or 1Password (`security add-generic-password …`) so rebuilds work without manual prompts.
## Managing secrets
- Declare secrets in Home Manager under `rsydn.secrets.secrets`. Each entry is forwarded to `sops.secrets.<name>` and defaults to writing `~/.config/secrets/<name>` with mode `0400`.
- Structure encrypted data however you like (`YAML`, `JSON`, `.env`). Only the encrypted file is tracked; plaintext never leaves `sops`.
- Keep sensitive values scoped. Per-project secrets usually live in `.env.sops` + `direnv`; long-lived device secrets belong here.
Example snippet for `modules/home/rsydn/secrets.nix` consumers:
```nix
rsydn.secrets.secrets = {
"openai-api-key" = {
sopsFile = ./secrets.sops.yaml;
path = "${config.xdg.configHome}/secrets/openai-api-key";
};
};
```
Create or update secrets by running:
```bash
SOPS_AGE_KEY_FILE=$HOME/.config/sops/age/keys.txt \
sops secrets/secrets.sops.yaml
```
When Home Manager activates, the secret is decrypted to the target path. Source it in Nushell with something like:
```nu
let-openai-key = (open $env.XDG_CONFIG_HOME ++ "/secrets/openai-api-key" | str trim)
```
Because the secret stays on disk, prefer reading it only when needed (inside commands/functions) instead of exporting it globally. For project workflows, you can also load it into env vars temporarily:
```nu
def with-openai-key [cmd] {
let key = open $env.XDG_CONFIG_HOME ++ "/secrets/openai-api-key" | str trim
with-env { OPENAI_API_KEY: $key } { nu -c $cmd }
}
```
To attach multiple secrets at once, use structured data (e.g. a YAML mapping) and parse it with Nushell's `from yaml`.
## Rotating keys
If you regenerate your Age key, re-encrypt the file with the new recipient (`sops updatekeys secrets/secrets.sops.yaml`) and re-run `darwin-rebuild --dry-run --flake .#macbook-pro` to verify the deployment. Remember to remove old recipients so machines without access can no longer decrypt.
## Frequently asked questions
- **Where do encrypted files live?** In this repo under `secrets/*.sops.yaml`; they are safe to commit.
- **Where does plaintext live?** At runtime under `~/.config/secrets/*` (managed by Home Manager). Keep permissions tight and never add these paths to git.
- **How do I share secrets with another machine?** Copy the Age public key from that machine into the `recipients` list, re-run `sops updatekeys`, commit, then pull and rebuild on the other host.
- **Can I keep using `.env`?** Yes—encrypt it (e.g. `secrets/project.env.sops`) and load it with `sops exec-env` inside `direnv` so per-project shells receive the decrypted variables without storing them on disk.