シェルスクリプトでAPIキーとパスワードを安全に扱う設計|コマンド履歴・ログ・プロセスリストにシークレットを残さない方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトでAPIキーとパスワードを安全に扱う設計|コマンド履歴・ログ・プロセスリストにシークレットを残さない方法
「このスクリプト、パスワードが直書きしてあるんですが、大丈夫ですか」
同僚から指摘されてヒヤッとした経験はないでしょうか。

シェルスクリプトでの認証情報の扱いは、実務でよく見落とされる盲点です。コマンド履歴への記録、プロセスリストへの露出、デバッグログへの混入——気づかないうちに複数の経路でシークレットが外部に漏れます。

この記事では、APIキーやパスワードをシェルスクリプトで安全に扱うための設計パターンを解説します。コマンド履歴対策・環境変数経由での受け渡し・ファイル権限設計・read -s による対話入力・set +x によるデバッグログ制御まで、RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済みの実践例をまとめました。

この記事のポイント

・コマンドライン引数にシークレットを渡すと ps aux で全ユーザーに見える
・シークレットは環境変数か権限 600 のファイル経由で渡すのが基本設計
・read -s で入力をマスクし、コマンド履歴に残らないように設計できる
・set +x でシークレット処理前後のデバッグ出力を無効化できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜシェルスクリプトにシークレットを直書きしてはいけないのか

実務でよく見かけるNG例をまず確認しておきましょう。

#!/bin/bash # NG例: シークレットをスクリプトに直書き curl -u "admin:mysecret123" https://api.example.com/data

このような書き方には、4つの漏洩経路があります。

① コマンド履歴への記録
スクリプトから直接 curl -u "admin:..." を呼ぶと、シェルの ~/.bash_history にそのままコマンドが保存されます。history コマンドで端末にアクセスできるユーザーなら誰でも閲覧可能です。

② ps aux・プロセスリストへの露出
コマンドライン引数はLinuxのプロセステーブルに記録されます。別のターミナルで次のコマンドを実行すると、引数ごとパスワードが見えます。

$ ps aux | grep curl tomohiro 1234 0.0 0.0 ... curl -u admin:mysecret123 https://api.example.com/data

同じサーバーで作業する全ユーザーがこの出力を見られます。共有サーバーでは致命的なリスクになります。

③ set -x のデバッグログへの混入
set -x(xtrace)でデバッグ出力を有効にしたスクリプトでは、すべてのコマンドと引数が展開されてログに記録されます。

# set -x 有効時のログ出力例 + curl -u admin:mysecret123 https://api.example.com/data

ログファイルにリダイレクトしている場合、そのファイルにシークレットが永続的に残ります。

④ Gitリポジトリへの混入
スクリプトに直書きしたまま git commit すると、リモートリポジトリにシークレットが永続的に記録されます。コミット履歴から削除するにはリベースが必要で、共有リポジトリでは非常に手間がかかります。

シークレットを環境変数で渡す設計

コマンドライン引数を避け、環境変数経由でシークレットをスクリプトに渡す方法が基本設計です。

1. スクリプト側で環境変数を参照する

#!/bin/bash set -euo pipefail # 環境変数が設定されているか確認 if [[ -z "${API_KEY:-}" ]]; then echo "ERROR: 環境変数 API_KEY が設定されていません" >&2 exit 1 fi # 環境変数からシークレットを参照(引数には渡さない) curl -H "Authorization: Bearer ${API_KEY}" https://api.example.com/data

2. 実行時に環境変数をセットする方法

# 方法1: exportして実行(サブシェルにも引き継がれる) export API_KEY="mysecret123" ./script.sh # 方法2: 実行時のみ設定(このコマンドの子プロセスだけに有効) API_KEY="mysecret123" ./script.sh # 方法3: .env ファイルを source してから実行 source .env ./script.sh

3. コマンド履歴に残さない工夫

export API_KEY="..." 自体もコマンド履歴に残るため、次の方法で回避できます。

# HISTCONTROL=ignorespace を設定すると、行頭にスペースを付けたコマンドが履歴に残らない export HISTCONTROL=ignorespace # 行頭にスペース(半角1文字)を付けて入力 export API_KEY="mysecret123"

ただし、この方法は運用ルールとして徹底が必要です。次のセクションで紹介するファイル経由の管理のほうが確実です。

権限付きシークレットファイルの設計

シークレットを権限 600 のファイルに保存して読み込む方法は、環境変数の直打ちよりも安全で、チーム運用でも扱いやすい設計です。

1. シークレットファイルの作成と権限設定

# シークレットファイルを安全な権限で作成 # umask 0177 = 所有者のみ read/write(600)、グループ・他ユーザーは全て禁止 old_umask=$(umask) umask 0177 cat > ~/.my-api-credentials << 'EOF' API_KEY=mysecret123 DB_PASSWORD=dbpassword456 EOF umask "${old_umask}" # umaskを元に戻す # 権限確認 ls -la ~/.my-api-credentials # -rw------- 1 tomohiro tomohiro 45 Sep 25 10:00 /home/tomohiro/.my-api-credentials

2. スクリプトからシークレットファイルを読み込む設計

#!/bin/bash set -euo pipefail CREDENTIALS_FILE="${HOME}/.my-api-credentials" # ファイルの存在確認 if [[ ! -f "${CREDENTIALS_FILE}" ]]; then echo "ERROR: シークレットファイルが見つかりません: ${CREDENTIALS_FILE}" >&2 exit 1 fi # ファイルの権限チェック(600 以外はエラー) file_perm=$(stat -c "%a" "${CREDENTIALS_FILE}") if [[ "${file_perm}" != "600" ]]; then echo "ERROR: シークレットファイルの権限が緩すぎます: ${file_perm}(期待値: 600)" >&2 echo " chmod 600 ${CREDENTIALS_FILE} で修正してください" >&2 exit 1 fi # source でシークレット変数を読み込む # shellcheck source=/dev/null source "${CREDENTIALS_FILE}" echo "APIに接続します..." curl -H "Authorization: Bearer ${API_KEY}" https://api.example.com/data

3. Gitへの混入を防ぐ.gitignore設定

シークレットファイルを含む可能性のあるパターンを .gitignore に必ず登録しておきます。

# .gitignore に追加するパターン .env .env.* *.credentials *.secrets .api-key

既存のコミットに誤ってシークレットファイルが含まれていないかを確認するには、git log --all -- .env でコミット履歴を遡れます。混入が発覚した場合は速やかにシークレットの値自体をローテーションするのが最優先です。

read -sで対話入力をマスクする設計

cronではなく手動実行スクリプトでパスワードを入力させたいとき、read -s を使うと入力文字を画面に表示せずに変数に受け取れます。

1. read -s の基本構文

#!/bin/bash # -s: 入力を表示しない(silentモード) # -r: バックスラッシュのエスケープ処理を無効化(推奨) # -p: プロンプト文字列 read -r -s -p "DBパスワードを入力してください: " DB_PASSWORD echo # read -s は改行を出力しないため、echoで改行を補う if [[ -z "${DB_PASSWORD}" ]]; then echo "ERROR: パスワードが空です" >&2 exit 1 fi

実行例:

$ ./script.sh DBパスワードを入力してください: (入力しても画面に表示されない)

2. read -sの結果をコマンドライン引数に渡さない

read -s で受け取った変数の値も、そのままコマンドライン引数に渡すとプロセスリストに出てしまいます。環境変数経由での受け渡しか、コマンド専用のシークレット渡し方法を使います。

# NG: 引数に渡すと ps aux でシークレットが見える mysql -u "${DB_USER}" -p"${DB_PASSWORD}" dbname # OK: MySQL は MYSQL_PWD 環境変数を認識する MYSQL_PWD="${DB_PASSWORD}" mysql -u "${DB_USER}" dbname # OK: psql は PGPASSWORD 環境変数を認識する PGPASSWORD="${DB_PASSWORD}" psql -U "${DB_USER}" dbname # OK: 設定ファイル経由(mysql の場合) # ~/.my.cnf に [client] password = xxx を記述、chmod 600 mysql --defaults-file="${HOME}/.my.cnf" dbname

注意: MYSQL_PWD はMySQL 5.7以降で非推奨の扱いになっており、警告が出ることがあります。定常的に使う場合は mysql_config_editor や .my.cnf への記録を検討してください。

set +xでデバッグログからシークレットを隠す設計

set -x(xtrace)は実行コマンドをリアルタイムで標準エラーに出力するデバッグ機能です。シークレットを扱う処理の前後でデバッグ出力を一時停止しないと、ログファイルにシークレットが混入します。

1. set +x で処理ブロックを囲む設計

#!/bin/bash set -euxo pipefail # set -x でデバッグ出力を有効化 echo "サービスを開始します..." # シークレットを扱うブロックだけ set -x を無効化 { set +x # デバッグ出力を無効化 source "${HOME}/.my-api-credentials" # この行はログに出ない API_KEY_LOCAL="${API_KEY}" # 変数コピーもログに出ない set -x # デバッグ出力を再開 } 2>/dev/null # set +x 自体の出力("+ set +x")を抑制 # ここから先は通常どおりデバッグ出力が有効 echo "API_KEY の先頭4文字: ${API_KEY_LOCAL:0:4}****"

2. ログ出力でシークレットをマスクする設計

ログにシークレットの存在を記録したい場合は、先頭数文字だけを表示するマスク変数を作る方法が有効です。

#!/bin/bash API_KEY="sk-abcdefghij1234567890" # マスク変数を作成(先頭4文字 + ****) masked_key="${API_KEY:0:4}****" # NG: ログにシークレットをそのまま出力 echo "API接続: キー=${API_KEY}" # OK: マスクした値のみをログに出力 echo "API接続: キー=${masked_key}" # 出力例: API接続: キー=sk-a****

シェルスクリプトでのシークレット安全設計をさらに体系的に学びたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で即使える設計パターンを実機ハンズオンで身につけられます。

トラブルシュート|よくあるエラーと対処

「変数を source したのに空になる」

source でシークレットファイルを読み込んでも変数が空になる場合、次の原因が多いです。

# 原因1: ファイルのパスが間違っている source ~/.my-api-credentials # ~ がホームディレクトリに展開されているか確認 # 原因2: .env ファイルの書式が間違っている # NG: export や引用符が含まれる形式(shell依存の問題が起きやすい) export API_KEY="mysecret" # export は不要 # OK: KEY=VALUE 形式のみ(引用符なし) API_KEY=mysecret # 原因3: サブシェルで source しても親プロセスに伝わらない $(source ~/.my-api-credentials) # NG: コマンド置換は子プロセス source ~/.my-api-credentials # OK: 現在のシェルで直接実行

「read -sがTTYエラーになる」

# エラー例 read: warning: read: -s: ignored with non-terminal input # 原因: cron やパイプ経由の実行では TTY がない # 対策: TTY の有無を確認してから read -s を使う if [[ -t 0 ]]; then # 端末から実行された場合のみ read -s を使う read -r -s -p "パスワード: " DB_PASSWORD echo else # TTYなし環境(cron等)ではファイル経由でシークレットを取得する source "${HOME}/.my-api-credentials" fi

「権限チェックで意図しないエラーが出る」

# stat -c "%a" はGNU coreutils版(Linux)の書式 # macOSでは stat -f "%OLp" を使う # ポータブルに書くにはOS判定が必要 if [[ "$(uname)" == "Darwin" ]]; then file_perm=$(stat -f "%OLp" "${CREDENTIALS_FILE}") else file_perm=$(stat -c "%a" "${CREDENTIALS_FILE}") fi

本記事のまとめ

シークレットの漏洩経路 対策
コマンドライン引数(ps aux に露出) 環境変数・設定ファイル経由に変更
コマンド履歴(~/.bash_history) HISTCONTROL=ignorespace + 行頭スペース、またはファイル管理
スクリプトへの直書き(Gitへの混入) .gitignore 登録 + 権限 600 のファイルに分離
対話入力の画面表示 read -r -s -p "プロンプト" でマスク入力
set -x のデバッグログへの混入 { set +x; ...; set -x; } 2>/dev/null でブロック
ログファイルへのシークレット記録 ${API_KEY:0:4}**** でマスク変数を作成してから出力
シークレットファイルの権限不備 chmod 600 / umask 0177 で権限を設定
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

<PR>手元に置いて学びを深める1冊

マスタリングLinuxシェルスクリプト 第2版

シェルスクリプトのセキュリティ設計・シークレット管理から自動化の実装パターンまで体系的に学べる、現場エンジニア必携の一冊。

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。