同僚から指摘されてヒヤッとした経験はないでしょうか。
シェルスクリプトでの認証情報の扱いは、実務でよく見落とされる盲点です。コマンド履歴への記録、プロセスリストへの露出、デバッグログへの混入——気づかないうちに複数の経路でシークレットが外部に漏れます。
この記事では、APIキーやパスワードをシェルスクリプトで安全に扱うための設計パターンを解説します。コマンド履歴対策・環境変数経由での受け渡し・ファイル権限設計・
read -s による対話入力・set +x によるデバッグログ制御まで、RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済みの実践例をまとめました。この記事のポイント
・コマンドライン引数にシークレットを渡すと ps aux で全ユーザーに見える
・シークレットは環境変数か権限 600 のファイル経由で渡すのが基本設計
・read -s で入力をマスクし、コマンド履歴に残らないように設計できる
・set +x でシークレット処理前後のデバッグ出力を無効化できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトにシークレットを直書きしてはいけないのか
実務でよく見かけるNG例をまず確認しておきましょう。#!/bin/bash # NG例: シークレットをスクリプトに直書き curl -u "admin:mysecret123" https://api.example.com/data
① コマンド履歴への記録
スクリプトから直接
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****
トラブルシュート|よくあるエラーと対処
「変数を 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 で権限を設定 |
シェルスクリプト講座を見る >>
<PR>手元に置いて学びを深める1冊
シェルスクリプトのセキュリティ設計・シークレット管理から自動化の実装パターンまで体系的に学べる、現場エンジニア必携の一冊。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトをマルチOS対応に設計する方法|RHEL・Ubuntu・Alpineを1本で動かすOS検出とパッケージ管理の抽象化
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます