どうもTORAです。
AIと一緒にアプリを作るとき、ログイン画面や外部APIの呼び出しを頼むことがあります。ただ、そのときに出てくる「パスワード」や「キー」は、性質の違うものが混ざっています。全部を同じように扱うと、置いてはいけない場所に置いてしまうことがあります。
この記事では、AIで個人開発している人に向けて、パスワード管理の基礎をまとめます。自分の開発アカウント、作ったアプリの利用者のログイン、アプリが使うAPIキーや接続用の秘密。この3つを分けて、それぞれどこに置くのか、AIに何を頼んで、人が何を確認するのかを書きます。根拠にしたのは、Webセキュリティの指針を公開している非営利団体OWASPの文書、米国の標準化機関NISTの認証ガイドライン、それに各サービスの公式ドキュメントです。
まず、秘密を3種類に分ける
1つ目は、開発者である自分のアカウントのパスワードです。GitHub、クラウドの管理画面、データベースのサービス、ドメインやメールのアカウントなどがこれにあたります。自分があとで入力して使うものなので、元の値をなくさないよう安全に保管しておく必要があります。
2つ目は、作ったアプリの利用者がログインに使うパスワードです。こちらは、開発者が元の値を知っている必要がありません。ログインのときに「登録したときと同じものが入力されたか」を確かめられれば足ります。なので、元に戻せない形に変換して保存し、その形どうしで照合します。この変換をハッシュといいます。
3つ目は、アプリが外部のサービスを呼ぶときに使うAPIキーや、データベースの接続文字列(接続先とIDとパスワードをまとめた文字列)などです。APIキーは、外部サービスに「このアプリからの呼び出しです」と示すための文字列で、プログラムがそのまま相手に送ります。送るには元の値が要るので、ハッシュにはできません。代わりに、サーバーの中だけに置いて、使える権限も必要な範囲に絞ります。
分けるときの基準は、元の値をあとで誰が使うかです。自分が使うものはパスワード管理アプリへ、誰も元の値を使わないものはハッシュへ、プログラムが使うものはサーバー側の保管場所へ、という形になります。これを取り違えると、まずいことになります。利用者のパスワードを開発者が読める形で持っていると、漏れたときにそのまま悪用されます。APIキーをハッシュにしてしまうと、アプリが外部サービスを呼べなくなります。

図1:秘密を、元の値をあとで誰が使うかで3つに分けた図です。上から順番にやる手順ではなく、用途による分類です。
自分の開発アカウントは、パスワード管理アプリと多要素認証で
AIで開発していると、GitHub、ホスティング、データベース、AIのAPIと、アカウントがどんどん増えます。どれか一つに入られるだけで、ソースコードやデプロイの設定、そこに保存した秘密まで触られるおそれがあります。
パスワード管理アプリは、サービスごとのIDとパスワードを暗号化して保管し、ログイン画面で入力まで手伝ってくれるアプリです。たいていは長いランダムなパスワードを作る機能も付いているので、サービスごとに別々のパスワードを作って、覚えるのはアプリに任せられます。NISTのガイドライン(SP 800-63B-4)でも、パスワード管理アプリを使うと強いパスワードが選ばれやすくなることが示されています。
そのうえで、多要素認証(MFA)をオンにします。たとえば、パスワードに加えてスマホの認証アプリに出るコードを求めるように、種類の違う要素を組み合わせて本人確認する仕組みです。OWASPの認証の指針は、MFAをパスワードがらみの攻撃の大半に対するいちばんの防御と位置づけています。NISTは、パスワードそのものはフィッシング(偽サイトに入力させて盗む手口)に強くない、と明記しています。選べるサービスでは、偽サイトに強いパスキーやセキュリティキーも候補になります。パスキーはパスワードなしで使える場合もあり、パスワードに必ず足すものではありません。スマホをなくしたときのための復旧コードも、パスワード管理アプリなど安全な場所に保管しておきます。
利用者のログインは、まず認証サービスに任せる
ここで言葉を二つ分けておきます。認証は「この人は誰か」を確かめること、つまりログインです。認可は「その人がどのデータや操作にアクセスしてよいか」を決めることです。ログインできた人が他人の注文履歴まで見られるなら、認証はできていても認可が壊れています。
ログインを自分でゼロから作ると、パスワードの保存と照合、再設定メール、失敗回数の制限、ログイン状態の管理まで、全部を自分で正しく実装しなければなりません。AIに頼めばそれらしいコードはすぐ出てきます。ただ、どこかに穴があっても普通に動いてしまうので、動いているかどうかだけでは判断できないんですよね。なので最初は、既存の認証サービスや、長く保守されている認証ライブラリを使うことを考えた方がいいと思います。認証サービスは、パスワードの保存、パスキー、Googleアカウントなどでのログイン、MFAといったログインの仕組みをまとめて提供してくれるサービスです。たとえばSupabase Authは、こうしたログイン機能を提供しています。
「Googleでログイン」のようなボタンには、OpenID Connect(OIDC)という仕組みが使われます。OIDCは、OAuthの上に本人確認の層を足したものです。ログインした人が誰なのかを、IDトークンという署名付きのデータで確かめます。OAuthのほうは、もともと「このアプリに、APIへのアクセスを任せる」ための認可の仕組みです。OWASPも、ログインにはOIDC、APIへのアクセス許可にはOAuth、という使い分けを示しています。Googleログインだけで運用するなら、アプリは利用者のGoogleのパスワードを受け取りませんし、自分でパスワードを保存する必要もありません。
パスキーは、公開鍵暗号を使うログイン方式です。秘密鍵は利用者側の端末や同期の仕組みで管理され、ログインのたびにその鍵でサーバーからの問いかけに署名します。サイト側にパスワードのような共有の秘密を置かないので、データベースから使い回しのきくパスワードが漏れる、ということが起きません。
ただ、認証サービスに任せても、こちらの仕事が全部なくなるわけではありません。パスワードの最小文字数やMFA、メールアドレス確認の設定、他人のデータを見せないための認可、アカウントを取り戻すときの流れ、ログアウトやパスワード変更時にログイン状態をどう無効にするか。このあたりは自分のアプリ側で決めて、確認する必要があります。
自前でパスワードを保存するなら
パスワードは、そのまま(平文)で保存しません。暗号化も、基本的には使いません。暗号化は鍵があれば元に戻せるので、鍵と一緒に盗まれれば元のパスワードが出てきてしまいます。OWASPのパスワード保存の指針でも、パスワードはハッシュで保存し、暗号化は元の値がどうしても必要な例外的な場合に限るとしています。
ここで注意したいのが、ハッシュなら何でもいいわけではないことです。SHA-256のような一般的なハッシュは計算が速く、ハッシュが盗まれると、攻撃者はよくあるパスワードを片っ端から高速に試せてしまいます。なので、わざと計算に時間とメモリがかかるように作られた、パスワード専用のハッシュ関数を使います。OWASPが第一候補にしているのはArgon2idです。最低限の設定の一つとして、メモリ19MiB、反復回数2、並列度1が示されています(ほかの最低構成も載っています)。
あわせて、ソルトも使います。ソルトはパスワードごとに作るランダムな値で、ハッシュを計算するときに一緒に混ぜます。これがあると、同じパスワードを使っている利用者がいても保存されるハッシュは別々になり、あらかじめ計算しておいた対応表で一気に解読される攻撃も防げます。よく使われているライブラリはソルトを自動で作って保存してくれるので、自分で生成処理を書く必要は基本的にありません。ハッシュの照合も、言語やフレームワークが用意している検証用の関数を使います。
それから、Argon2idという名前が入っていれば安心、というわけではありません。設定値は、本番と同じ環境のサーバーで実際に試して決めます。重すぎると、ログイン要求が大量に来たときにサーバーの処理が追いつかなくなります。OWASPの目安は、1回の計算が1秒未満に収まることです。
パスワードの長さと入力ルール
文字数のルールでよく参照されるのが、2025年7月に最終版が出たNIST SP 800-63B-4です。米国の政府システム向けのガイドラインなので、個人開発に守る義務があるわけではありません。それでも、数字の根拠として参考にできます。
NISTは、パスワードだけでログインできる場合(認証の要素がパスワード1つだけの場合)は、最低15文字を要求するよう定めています。8文字まで短くしてよいのは、そのパスワードが多要素認証の一部としてだけ使われる場合に限られます。注意したいのは、MFAを「使いたい人だけオンにできる」設定にしているケースです。MFAをオンにしていない利用者にとって、パスワードは唯一の要素です。なので、MFAを選べるようにしただけでは、全員の最低文字数を8文字に下げる理由になりません。
ほかにも、NISTには次のような項目があります。
- 最大長は少なくとも64文字まで受け付ける
- 「大文字・数字・記号を必ず混ぜる」といった組み合わせのルールを課さない
- 定期的なパスワード変更を求めない。ただし漏えいの証拠があれば変更させる
- よく使われるパスワードや、過去の漏えいで知られたパスワードは登録させない
- 失敗したログインの回数を制限する
- パスワード管理アプリの自動入力を妨げない。貼り付けもできるようにする
ログインまわりで確認しておくこと
失敗回数の制限は、ログインを何度でも試せる状態にしないための仕組みです。OWASPは、失敗回数を接続元のIPアドレスではなくアカウントごとに数えるよう勧めています。IPアドレスを次々に変えながら試されると、IP単位の制限ではすり抜けられてしまうからです。一方で、アカウントを固定時間ロックする方式は、他人のアカウントをわざとロックさせる嫌がらせにも使えます。そのため、失敗するたびに待ち時間を延ばすやり方なども紹介されています。
エラーメッセージは「IDまたはパスワードが違います」のようにまとめます。「このメールアドレスは登録されていません」と出してしまうと、どのアドレスが登録済みかを調べる手がかりになります。再設定の画面でも同じで、「そのメールアドレスが登録されていれば、再設定のメールを送ります」のような書き方にします。
ログイン画面と、ログイン後のすべてのページはHTTPSで配信します。HTTPSでないと、通信の途中でパスワードやログイン状態を示す値を盗み見られるおそれがあります。
再設定のメールに入れるリンクは、OWASPのパスワード再設定の指針に沿って、短い時間で期限切れになり、一度使ったら無効になるようにします。再設定やパスワード変更のあとは、ほかの端末に残っているログイン状態も無効にします。
ログイン状態は、多くの場合セッションという仕組みで保ちます。ログインのあと、サーバーが「この人はログイン済み」と覚えておき、ブラウザはそれを示すセッションIDやトークンを毎回送ります。OWASPのセッション管理の指針にあるとおり、ログアウトでは、ブラウザ側の値を消すだけでなく、サーバー側でもそのセッションを無効にします。パスワードやメールアドレスの変更のように影響の大きい操作では、現在のパスワードやMFAでもう一度本人確認をします。共用のPCでログアウトし忘れたセッションを他人に使われても、パスワードまで変えられないようにするためです。
認可は、ログイン機能とは別に確かめる必要があります。よくあるのは、URLやAPIの中にある注文番号やユーザーIDを書き換えると、他人のデータが返ってきてしまうケースです。画面上でボタンを隠していても、APIは直接呼べてしまいます。なので、データを返す処理のたびに、サーバー側で「ログインしている人が、そのデータの持ち主か、扱ってよい人か」を確認します。
秘密のAPIキーはサーバーだけに置く
ブラウザに届いたHTMLやJavaScript、通信の中身は、利用者がブラウザの開発者ツールで全部見られます。なので、秘密のAPIキーはブラウザに渡るコードに入れられません。
ここでつまずきやすいのが、フレームワークの環境変数の扱いです。Next.jsの環境変数のガイドでは、NEXT_PUBLIC_ で始まる変数は、ビルドのときに値がブラウザ向けのJavaScriptへ直接埋め込まれると説明されています。.env ファイルに書いたから秘密になる、というわけではありません。この接頭辞のない変数は、標準ではサーバー側でしか読めません。ほかのフレームワークでは接頭辞のルールが違うので、使っているものの公式ドキュメントで確認してください。
外部APIを呼ぶときは、ブラウザから直接呼ぶのではなく、ブラウザ→自分のサーバー→外部API、という順番にします。秘密のキーを持つのはサーバーだけです。

図2:秘密のAPIキーをサーバーだけが持つ構成の概念図です。公開可能キーをブラウザで使う場合も、そのキーでどのデータに届くかは別のアクセス制御で決まります。
ただし、自分のサーバーに作った中継用のAPIも、インターネットから誰でも呼べます。ここに認証、認可、利用制限がないと、第三者が中継APIを通して、自分の契約している外部APIを好きなだけ使えてしまいます。利用制限とは、同じ人や同じ接続元から短い時間に送れる回数や量の上限のことです。AIのAPIのように使った分だけ料金がかかるサービスでは、特に入れておきたいところです。
サーバー側の変数にしていても、その値をAPIの応答やエラーメッセージ、ログに出してしまえば漏れます。キーの値はログに書かないようにします。
公開してよいキーと、だめなキー
サービスによっては、ブラウザに置いてよいキーがあります。たとえばSupabaseのAPIキーの説明では、publishable key(公開可能キー)はブラウザやモバイルアプリに入れてよいもので、secret key(秘密キー)はサーバーなど自分が管理する場所だけで使うものとされています。
公開してよいキーがあるからといって、データを公開してよいわけではありません。Supabaseの公開可能キーで届くのは、RLS(Row Level Security)が許可した範囲だけです。RLSは、データベースの行ごとに、誰が読めて誰が変更できるかを決めるルールです。逆に言えば、RLSを有効にしていない、またはルールが緩いテーブルは、公開可能キーを持つ誰からでも読めてしまうことがあります。テーブル自体へのアクセス権限の設定にもよるので、公開前に確認します。秘密キーはRLSをすべて飛ばしてプロジェクトの全データにアクセスできるので、絶対にブラウザへ出しません。
キーの名前だけを見て、秘密かどうかを判断するのはやめておいた方がいいです。サービスごとに公式ドキュメントで確かめます。Supabaseでも、旧方式の anon / service_role と現在の公開可能キー・秘密キーがあるので、AIの説明がどちらを対象にしているか確認します。
開発用と本番用のキーは分けます。権限を絞れるサービスでは、必要な範囲だけにします。Supabaseは、サーバー側の部品ごとに別々の秘密キーを使うよう勧めています。そうしておけば、一つが漏れても入れ替えるのはそのキーだけで済みます。OWASPの秘密情報管理の指針も、秘密に触れられる人や仕組みを必要最小限にすること(最小権限)を求めています。
.envと.gitignoreだけでは足りない理由
.env は、環境変数をまとめて書いておくファイルです。フレームワークが起動時に読み込んでくれるので便利ですが、ただのテキストファイルで、暗号化された金庫ではありません。.gitignore は、Gitに「このファイルは管理しなくていい」と伝えるための設定ファイルです。Next.jsの雛形でも、最初から .env 系のファイルが .gitignore に入っています。
リポジトリに入れるのは、変数名だけを書いた見本のファイルにしておくと分かりやすいです。値はダミーにします。
# .env.example(値はダミー)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxxxxxxx
SUPABASE_SECRET_KEY=
# .gitignore
.env
.env.*
!.env.example
ただ、この二つで守れる範囲には限りがあります。一つはGitの履歴、もう一つはAIが読める範囲です。
限界1:すでにコミットしたものは消えない
.gitignore が防ぐのは、まだGitが管理していないファイルを新しく追加してしまうことだけです。GitHubのドキュメントにあるとおり、すでに一度コミットしたファイルは、あとから .gitignore に書いても管理され続けます。管理対象から外すには、次のコマンドを使います。
git rm --cached .env
これで外れるのは、これから先のコミットだけです。手元のファイルは残ります。過去のコミットには値がそのまま残っていて、GitHubにpushしていれば、誰かのクローンやフォークにコピーされているかもしれません。履歴の書き換えは、ほかの人のクローンを壊すなど副作用が大きく、GitHubの機密データ削除の手順でも、他人のクローンやフォークからは消せないとされています。一度でもコミットしてpushした秘密は、漏れたものとして扱った方が安全です。対応は後半の「漏れたかもしれないとき」で書きます。
限界2:AIは.gitignoreに関係なくファイルを読める
OWASPのAIコーディング向けの指針ははっきり書いていて、.gitignore が効くのはGitに対してだけです。AIツールはファイルを直接読みます。しかもAIコーディングのツールは、開いているファイルやプロジェクトの構成、ターミナルの出力などを、AIモデルを提供している会社へ文脈として送ることがあります。今開いているファイルだけが送られる、とは限りません。
なので、AIツールを使っている間は次のことに気をつけます。
- AIが動いているエディタで
.envや秘密鍵のファイルを開かない - APIキーをチャット欄に貼らない。AIがターミナルを見られる状態のときは、ターミナルにも貼らない
- ツールごとの除外設定(
.cursorignoreなどの読み込み除外ファイル)を使う。ただし、何がどこまで除外されるかはツールによって違うので、公式の説明で確認する - 本番のキーは、AIエージェントが読めるプロジェクトのフォルダや環境に置かない。手元の開発では、権限を絞った開発用のキーを使う
- AIには変数名とダミーの値だけを渡して、コードを書いてもらう
プロンプトに「.envは読まないで」と書いても、読めなくなるわけではありません。それはお願いであって、OSやツールの権限で読めないようにする設定とは別ものです。
それから、プロンプトインジェクションにも触れておきます。AIに読ませた資料の中に指示が紛れ込んでいて、AIがその指示に従って意図しない操作をしてしまう攻撃のことです。issueの本文やREADME、AIに調べさせたWebページ、エラーメッセージなど、AIエージェントが読むものはすべて入口になり得ます。そのときエージェントが秘密を読めて、外部への通信もでき、確認なしに操作を実行できる状態だと、被害が大きくなります。OWASPは、こうした外部の文章を信頼できない入力として扱うこと、エージェントの読める範囲や実行できる操作を絞ること、隔離した環境で動かすことを勧めています。
AIに頼むこと、人が確認すること
AIに実装を頼むときは、秘密の扱いを最初に条件として渡しておくと、あとで直す量が減ります。次はNext.jsを使う場合の依頼例です。〇〇 の部分は自分の構成に合わせて置き換え、ほかのフレームワークでは公開変数のルールも変更します。
このアプリのログインには〇〇(使う認証サービス)を使います。
パスワードの保存や照合の処理は自分で実装しないでください。
秘密のAPIキーは、サーバー側のコードだけで環境変数から読んでください。
ブラウザに届くコードには秘密の値を置かず、NEXT_PUBLIC_ を付けるのは公開してよい値だけにしてください。
コードには変数名だけを書き、値は書かないでください。
データを返すAPIでは、ログイン中のユーザーがそのデータの持ち主かをサーバー側で確認してください。
外部APIを中継するAPIには、ログイン確認と回数制限を入れてください。
最後に、変更したファイルの一覧と、秘密の値がどこで読まれてどこへ渡るかを説明してください。
ここで気をつけたいのは、AIが書いたコードとテストが両方通っていても、それだけでは安心できないことです。OWASPのAIコーディング向けの指針も、コードとテストを同じAIが書いた場合、テストが全部通っていても独立した確認にはならないとしています。認証や認可のテストは、人が自分で書くよう勧めています。
以前、テストもレビューも通っていたのに設計が違っていた話と、カバレッジ100%でCIもグリーンなのに、SQLの半分近くにテストがなかった話を書きました。テストが通っていることと、確かめたいことのテストが存在することは別です。セキュリティでも同じで、「他人のIDでデータを取りにいったら拒否されるか」のようなテストが本当にあるかは、人が確かめる必要があります。
公開前には、次の項目を一つずつ確認します。
- 自分のGitHub、クラウド、データベースのサービスなどで、パスキーやMFAを設定している。パスワードを使う場合は管理アプリで生成・保管し、使い回していない
- ブラウザの開発者ツールで、配信されたJavaScriptと通信の中に秘密のキーが入っていない
NEXT_PUBLIC_などブラウザに公開される変数に、秘密の値を入れていない.envがGitの管理対象になっておらず、過去のコミットにも秘密が入っていない- 公開可能キーで読めるデータを、RLSなどのアクセス制御で絞っている
- 別のユーザーでログインしてIDを書き換えると、他人のデータが返らずに拒否される。そのテストがある
- 外部APIの中継には、ログイン確認と利用制限がある
- ログイン失敗の回数制限、まとめたエラーメッセージ、HTTPS、再設定リンクの期限と一回限りの利用、ログアウト時のセッション無効化を確認した
- 自前でパスワードを保存している場合は、Argon2idなどのパスワード専用ハッシュを、本番環境で試した設定で使っている
- 開発用と本番用でキーを分け、権限を絞っている。ログにキーの値を出していない
- AIが提案したパッケージが実在する正規のものか、既知の脆弱性がない版かを確認した
- AIが変更したファイルを、依頼した範囲の外も含めて全部見た
漏れたかもしれないときは、まず無効化する
キーをコミットしてpushしてしまった、ブラウザ向けのコードに入れたまま公開していた。そういうことに気づいたら、最初にやるのは、その秘密を発行したサービスの管理画面での無効化か、新しい値への入れ替え(ローテーション)です。GitHubのドキュメントも、漏れたのがパスワードやトークンなら最初の手順は無効化かローテーションで、それで十分な場合もあると書いています。履歴の書き換えは、そのあと必要かどうかを考える作業です。
アプリを止めずに入れ替える順番を案内しているサービスもあります。Supabaseは漏れたキーの入れ替えとして、新しいキーを作る→アプリの全部の場所で差し替える→全部が新しいキーで動くことを確かめる→古いキーを削除する、という手順を示しています。同じページには、入れ替える前に漏れた原因を直すこと、漏れたキーはすぐ削除することも書かれています。原因が残ったままだと、新しいキーも同じ経路で漏れてしまいます。不正利用が進んでいるなど被害を止める必要がある場合は、全部の動作確認を待たずに漏れたキーを無効にします。
無効にしたら、そのサービスの利用ログや請求の画面で、身に覚えのない使われ方がないかを見ます。不審な利用があれば、そのサービスのサポートに相談します。
自分のアカウントのパスワードが漏れた疑いがあるときは、パスワードを変えて、MFAの設定を確かめます。ログイン中の端末やセッションの一覧を見て、知らないものはログアウトさせます。作成済みのトークンや登録したSSH鍵に覚えのないものがないかも確認します。
アプリの利用者のパスワードやデータに影響したかもしれない場合は、利用者にパスワードの再設定を求めます。NISTも、漏えいの証拠があれば変更させるよう定めています。既存のセッションも無効にして、利用者への連絡が必要かどうかも検討します。
キーを入れ替えて履歴を消しても、それで漏れていなかったと証明できるわけではありません。なので「消したから大丈夫」で終わらせず、無効化と、使われた形跡の確認までやっておきます。
