NixOS で API key を暗号化しつつ自動的に環境変数に設定する方法

概要

前置き

Neovim に AI 機能を追加できるプラグインが複数登場していますので、その1つである codecompanion.nvim を使ってみようと思いました。

ただ、AI 機能を使うには API 呼び出しが必要で、codecompanion.nvim で API を呼び出すには API key を環境変数にセットする必要があります。export HOGE_API_KEY=**** と入力すれば API key を環境変数にセットできますが、コマンド履歴に API key が残りますし、ログインするたびに実行するのは面倒です。しかし、Nix の設定ファイルに API key を書き込む方法では、誤ってそのファイルを Github にプッシュして漏洩する可能性があります。

そこで対応策を探したところ、Mozilla 財団が暗号化されたファイルを扱うためのエディタを開発しており、それを NixOS に組み込むことで機密情報を安全に管理する方法があることが分かりましたので、導入することにしました。

しかし、実際に使えるようになるまで相当苦労しましたので、再度同じ作業をする羽目になったときに備えて備忘録として手順などをまとめます。

環境

OS

NixOS on WSL2 (Windows on ARM)

Nix

1> nix-shell -p nix-info --run "nix-info -m"
2 - system: `"aarch64-linux"`
3 - host os: `Linux 5.15.167.4-microsoft-standard-WSL2, NixOS, 25.05 (Warbler), 25.05.20250503.f21e454`
4 - multi-user?: `yes`
5 - sandbox: `yes`
6 - version: `nix-env (Nix) 2.28.3`
7 - channels(root): `"nixos-24.11, nixos-wsl"`
8 - nixpkgs: `/nix/store/qmm7hgw60vp7vj9lma95hl329d0j3n6n-source`

home-manager

1> home-manager --version
225.05-pre

home-manager は、Flakes を利用してスタンドアロン型で導入しています。

シェル

1> zsh --version
2zsh 5.9 (aarch64-unknown-linux-gnu)

大まかな作業の流れ

大まかな作業の流れは、暗号化したい API key を書き込んだファイルを sops 経由で作成し、ファイルを作成したら即座に age で暗号化し、暗号化したファイルを sops-nix による復号化を経由して NixOS で読み取るというものになります。

なお、この作業では秘密鍵や API key などの機密情報を扱いますので、設定ファイルを Git で管理している場合、機密情報をどう管理するかが問題となります。この点は最後に説明しますが、重要な点を申し上げておくと、この作業で API key や秘密鍵を格納したファイルは Git の管理下におく必要がある、言い換えると git add でステージングできるようにしておく必要があります。

理由は、この作業では Flakes で設定ファイルを取り扱いますが、Flakes は Git で管理していないファイルを取り扱えないため、機密情報を格納したファイルであっても Git で管理できる必要があるためです。言い換えると、機密情報を格納したファイルを .gitignore に列挙してはダメということです。私はこの点になかなか気付かず時間を溶かしてしまいました

必要なアプリなどのインストール

必要なアプリなどは以下の3つです。

アプリの sops が前置きで紹介した Mozilla 財団開発の暗号化ファイルエディタで、モジュールの sops-nix は sops を NixOS に組込むためのモジュールです。また、age は、Go 言語で書かれたシンプルかつモダンな暗号化ツールです。

なお、SSH キーから必要な鍵を生成する ssh-to-pgp というアプリもありますが、パスフレーズを設定した SSH キーから鍵を生成できないため、今回は使っていません。

インストール

まずアプリからインストールします。アプリは home-manager でインストールしますので、home.nix を以下のとおり編集してから home-manager switch . コマンドでインストールします。

1home.packages = with pkgs; [
2+  age
3+  sops
4];

それからモジュールをインストールします。公式リポジトリでは Flakes を使う方法が推奨されていますので、それに従って flake.nix を以下のとおり編集します。

 1{
 2  inputs = {
 3    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
 4    home-manager.url = "github:nix-community/home-manager/";
 5    home-manager.inputs.nixpkgs.follows = "nixpkgs";
 6+   sops-nix.url = "github:Mic92/sops-nix";
 7  };
 8
 9  outputs =
10    inputs:
11    let
12      # 各マシンのシステムタイプを定義する辞書
13      systems = {
14        desktop = "x86_64-linux";
15        zenbook = "aarch64-linux";
16      };
17
18      # NixOS のシステム構成を作成するヘルパー関数
19      mkNixosSystem =
20        machine: system:
21        inputs.nixpkgs.lib.nixosSystem {
22          system = system;
23          modules = [
24            ./configuration.nix
25+           inputs.sops-nix.nixosModules.sops
26          ];
27        };
28
29      # Home Manager のホーム構成を作成するヘルパー関数
30      mkHomeManager =
31        machine: system:
32        inputs.home-manager.lib.homeManagerConfiguration {
33          pkgs = import inputs.nixpkgs {
34            system = system;
35            config = {
36              allowUnfree = true;
37            };
38          };
39          extraSpecialArgs = { inherit inputs; };
40          modules = [
41            ./home.nix
42          ];
43        };
44    in
45    {
46      nixosConfigurations = builtins.listToAttrs (
47        map (machine: {
48          name = machine;
49          value = mkNixosSystem machine (systems.${machine});
50        }) (builtins.attrNames systems)
51      );
52      homeConfigurations = builtins.listToAttrs (
53        map (machine: {
54          name = "s-show@" + machine;
55          value = mkHomeManager machine (systems.${machine});
56        }) (builtins.attrNames systems)
57      );
58+     home-manager.sharedModules = [
59+       inputs.sops-nix.homeManagerModules.sops
60+     ];
61    };
62}

flake.nix を編集したら、cd .dotfiles && sudo nixos-rebuild switch --flake .#zenbook --impure --show-trace を実行して設定を反映させます。反映後は、念のために NixOS を再起動します。

API key の暗号化

API key の暗号化の手順は次のとおりです。

  1. 暗号化と復号化に使う公開鍵と秘密鍵のペアを作成する
  2. 暗号化のルールを作成する
  3. sops コマンド経由で API key を暗号化して保存する

鍵のペアを作成

まず、暗号化と復号化に使う鍵を格納したファイルを格納するためのディレクトリを用意します。

1mkdir -p ~/.dotfiles/sops/age

それから、鍵ペアを生成してファイルに保存します。

1age-keygen -o ~/.dotfiles/sops/age/keys.txt
2# ここに公開鍵が表示される

コマンド実行後に公開鍵が表示されますので、どこかにコピーしておきます。コピーし忘れたときは、age-keygen -y ~/.dotfiles/sops/age/keys.txt で確認できます。

暗号化のルール作成

touch ~/.dotfiles/.sops.yaml で暗号化のルールを記述するためのファイルを作成したら、以下のとおりルールを記述します。

1# zenbook は私の環境のホスト名なので、適宜読み替えてください。
2keys:
3  - &zenbook (先程コピーした公開鍵を入力)
4creation_rules:
5  - path_regex: secrets.yaml$
6    key_groups:
7    - age:
8      - *zenbook

API key の暗号化

以下のコマンドを実行すると API key を保存するファイルのテンプレートが $EDITOR で指定しているエディタで開きます。

1# `secrets.yaml` は `.sops.yaml` の `path_regex` に合わせる
2nix-shell -p sops --run "sops secrets.yaml"

ファイルが開いたら、環境変数の名前と API key を入力します。

1# この段階では API key をそのまま入力して保存する 
2# 保存して閉じたら自動的に暗号化される
3OPENROUTER_API_KEY: '***'

これで暗号化された API key が保存されました。

暗号化された API key を読み込む

home.nix を編集して、暗号化された API key を Nix で読み込んで環境変数にセットします。まず、home.nix を以下のとおり編集します。

 1home.packages = with pkgs; [
 2  ~~~
 3];
 4
 5+ sops = {
 6+   age.keyFile = "/home/(username)/.dotfiles/sops/age/keys.txt"; # must have no password!
 7+   defaultSopsFile = ./secrets.yaml;
 8+   defaultSymlinkPath = "/run/user/1001/secrets";
 9+   defaultSecretsMountPoint = "/run/user/1001/secrets.d";
10+ };
11
12+ imports = [
13+   inputs.sops-nix.homeManagerModules.sops
14+ ];

編集したら home-manager switch . を実行して設定を反映させます。エラーが出なければ次の編集に移ります。

 1sops = {
 2  age.keyFile = "/home/(username)/.dotfiles/sops/age/keys.txt";
 3  defaultSopsFile = ./secrets.yaml;
 4  defaultSymlinkPath = "/run/user/1001/secrets";
 5  defaultSecretsMountPoint = "/run/user/1001/secrets.d";
 6+  secrets.OPENROUTER_API_KEY = {
 7+    path = "${config.sops.defaultSymlinkPath}/OPENROUTER_API_KEY";
 8+  };
 9};
10
11imports = [
12  inputs.sops-nix.homeManagerModules.sops
13];

編集したら再び home-manager switch . を実行して設定を反映させます。エラーが出なければ次の編集に移ります。なお、私のシェルは ZSH なので、ZSH に合わせた設定になっています。

1imports = [
2  inputs.sops-nix.homeManagerModules.sops
3];
4
5+ programs.zsh = {
6+   initExtra = ''
7+     export OPENROUTER_API_KEY=$(cat ${config.sops.secrets.OPENROUTER_API_KEY.path})
8+   '';
9+ };

編集後は home-manager switch . を実行して設定を反映させます。エラーが出なければ NixOS を再起動します。再起動後にシェルで echo $OPENROUTER_API_KEY を実行して環境変数が設定されているか確認します。上手く設定できていれば API key が表示されると思います。

なお、上記の説明では home.nix の設定を分割して適用していますが、最初は全部まとめて設定していました。しかし、何度試してもエラーになってしまうので、上記のとおり設定を分割して適用したところ上手くいきました。そのため、本記事では上記のとおり説明しています。

Git の設定

設定ファイルを Git で管理している場合、暗号化した API key が格納されている secrets.yaml や、秘密鍵が格納されている sops/age/keys.txt を GitHub にプッシュしてしまう事態を避ける必要があります。

しかし、これらのファイルを .gitignore に列挙することはできないのでどうしたものか悩んだのですが、とりあえず「プッシュするためにはコミットする必要があるので、コミットできないようにしてしまえば何とかなるだろう」と考えました。

そこで、Git の pre-hook 機能を使って「指定した名前のファイルをコミットしようとしたらブロックする」という機能を追加しました。具体的には、.git/hooks/pre-commit に以下のスクリプトを書きました。

 1#!/usr/bin/env bash
 2
 3# コミットをブロックするファイルのリスト
 4blocked_files="secrets.yaml keys.txt"
 5
 6# コミット対象のファイルを取得
 7staged_files=$(git diff --cached --name-only)
 8
 9for file in $staged_files; do
10  for blocked_file in $blocked_files; do
11    if [ "$(basename $file)" = "$blocked_file" ]; then
12      echo "機密保持のため $blocked_file のコミットは禁止!"
13      exit 1
14    fi
15  done
16done

これで secrets.yamlkeys.txt をコミットしようとするとブロックできるようになりました。洗練された方法とは言い難いですが、機密情報をプッシュする事態は避けられるのでとりあえずヨシとしています。

参考情報

Comparison of secret managing schemes - NixOS Wiki は NixOS で機密情報を扱うための手段を比較したページです。

Managing Secrets in NixOS Home Manager with SOPS は具体的な手順を紹介しているページです。ただ、このページは home-manager を NixOS のモジュールとしてインストールしているので、私のようにスタンドアロンでインストールしている場合はそのまま適用できません。本記事はこのページの情報を元に悪戦苦闘した結果でもあります。