【入門編】 AWS Lambdaの環境変数への機密情報埋め込みリスク – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!クラウドやセキュリティの世界へようこそ。

AWS Lambda(ラムダ)を使えば、サーバーの管理を一切気にすることなくプログラムを動かせて、本当に便利ですよね。「バックエンド処理は全部Lambdaにお任せ!」という開発現場もとても増えています。

しかし、開発がスムーズに進む一方で、「APIキーやデータベースのパスワードなどの秘密の情報を、どこに保存するか」という問題で、思わぬ落とし穴にハマってしまうケースが後を絶ちません。

一番手軽だからといって、Lambdaの「環境変数」に直接秘密の鍵を書き込んでいませんか?

実はこれ、ペネトレーションテスト(疑似侵入テスト)を行う攻撃側の視点から見ると、「ここに宝箱の鍵がありますよ!」と看板を立てているのと同じくらい危険な状態なのです。

今回は、新任のIT担当者さんや開発初心者の方に向けて、なぜ環境変数に機密情報をそのまま置くのが危ないのか、そしてどうやって安全に管理すればよいのかを、身の回りの防犯に例えながら優しく解説していきます。一緒に一歩ずつ学んでいきましょう!

—

1. 身近な防犯に例えると?「玄関マットの下の合鍵」問題

まずは、身近な「お家の防犯」に例えて考えてみましょう。

家に入るとき、いちいち鍵をカバンから探すのが面倒だからといって、「玄関のマットの下」や「郵便ポストの奥」に合鍵を隠していませんか?

【危険な状態のイメージ】
・お家(Lambda関数)
・ドアの鍵(APIキーやDBのパスワード)
・玄関マットの下(環境変数) ← ここに合鍵を置きっぱなし!

住人(開発者)からすれば「マットで隠れているから外からは見えないはず」と思いがちですよね。
しかし、泥棒(攻撃者)は「誰もが真っ先に隠しそうな場所」を熟知しています。玄関の前に立つことができたら、まずマットをめくってみるのが泥棒のセオリーなのです。

Lambdaの「環境変数」に直接書き込まれた機密情報は、まさにこの玄関マットの下の合鍵と同じ状態になってしまっています。

—

2. 攻撃者はどうやって環境変数を覗き見るの?

「でも、AWSの管理画面(マネジメントコンソール)のパスワードは堅牢だし、そう簡単に覗かれないのでは?」と思いますよね。

実は、攻撃者はAWSのログイン画面から堂々と入ってくるだけではありません。Webアプリのちょっとした隙(脆弱性)を突いて、裏側から覗き見ようとしてきます。よくある攻撃ルートを2つ見てみましょう。

攻撃ルート①:プログラムの不具合から「中を覗き見る」

例えば、Lambda上で動いているWebアプリケーションに、入力チェックの不備(OSコマンドインジェクションやディレクトリトラバーサルなど)があったとします。

攻撃者は、システムに「環境変数の一覧を出力して」と裏コマンド(Linuxの env や printenv コマンドなど)を送り込みます。すると、画面やログにAPIキーなどの秘密情報がポロッと表示されてしまうのです。

攻撃ルート②:開発者の権限を悪用する

プロジェクトに参加している開発者のAWSアカウントや、CI/CD(自動デプロイツール)の権限が少し広めに設定されていた場合、攻撃者はその権限を奪い取ろうとします。

もし、設定を読み取る権限(lambda:GetFunction や lambda:GetFunctionConfiguration など)があれば、AWS CLIからコマンドをたった1行叩くだけで、環境変数の中身が平文(暗号化されていない状態)で丸見えになってしまいます。

—

3. やってはいけない!危険な実装例

百聞は一見にしかずです。まずは「やってはいけない危険な書き方」を見てみましょう。Pythonを使ったLambda関数の例です。

import os
import requests

# ✕ やってはいけない例:環境変数から直接APIキーを取得
# 環境変数名: STRIPE_API_KEY などに直接「sk_live_xxxx...」と書かれている状態
API_SECRET_KEY = os.environ.get('STRIPE_API_KEY')

def lambda_handler(event, context):
    try:
        # 万が一プログラムで予期せぬエラーが起きたとき……
        response = requests.post(
            "https://api.example.com/charge",
            headers={"Authorization": f"Bearer {API_SECRET_KEY}"}
        )
        return {"statusCode": 200, "body": "Success"}
        
    except Exception as e:
        # エラーメッセージをそのまま画面に返してしまうと、
        # メッセージの中にAPIキーが混ざって漏洩する原因になります!
        return {"statusCode": 500, "body": str(e)}

このように書いてしまうと、AWSコンソールの設定画面にもAPIキーがそのまま表示されますし、前述の攻撃を受けた際に一発で漏洩してしまいます。

—

4. 正しい防犯対策:「貸金庫」からその都度鍵を取り出そう!

では、どうすれば良いのでしょうか?
答えはとてもシンプルです。合鍵を玄関マットの下に置くのをやめて、「銀行の貸金庫」に預ければいいのです。

AWSには、まさにこの貸金庫の役割を果たしてくれる素晴らしいサービスが用意されています。

  • AWS Systems Manager Parameter Store(パラメータストア)
  • 設定値やパスワードを安全に保管できる仕組み(SecureStringを選べば暗号化されます)。比較的安価(標準機能は無料枠あり)で手軽に使えます。
  • AWS Secrets Manager(シークレットマネージャー)
  • より機密性の高いデータベースパスワード等に特化したサービス。定期的にパスワードを自動変更(ローテーション)する機能なども備わっています。

安全な運用の仕組み

プログラムは起動したとき、自分の手元に合鍵を持っていません。
「身分証明書(IAMロール)」をAWSの貸金庫(Secrets Managerなど)に提示して、「私、正規のLambdaです!鍵を1つ貸してください」と都度リクエストし、メモリ上でのみ受け取って処理を行います。

これなら、もし玄関マット(環境変数)をめくられても、そこには何もありません。

—

5. 実践!安全なLambda関数の実装例

それでは、実際にAWS Systems Manager Parameter Storeを使って、安全に秘密情報を取得するコードを書いてみましょう。

手順1:IAMロールで「最小限の権限」を与える

Lambdaが貸金庫を開けられるように、Lambdaの「実行ロール(IAMロール)」に必要な権限だけを追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter"
            ],
            "Resource": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/my-app/api-key"
        }
    ]
}

※ Resource には、対象のパラメータのARNを正確に指定して、「このパラメータしか読めない」ように絞り込むのがポイントです。

手順2:Pythonコードから安全に取得する

Parameter Storeに、名前 /my-app/api-key、タイプ SecureString でキーを保存してある前提のコードです。

import boto3
import os
from botocore.exceptions import ClientError

# Parameter Storeの名前だけを環境変数に持たせる(これなら漏れても秘密鍵自体は見えない!)
PARAMETER_NAME = os.environ.get('KEY_PARAMETER_NAME', '/my-app/api-key')

# AWS SDK (Boto3) のクライアントを作成
ssm_client = boto3.client('ssm')

# 取得した秘密鍵をキャッシュ(一時保存)しておく変数
# (毎回取りに行くと通信のオーバーヘッドやコストがかかるため)
cached_api_key = None

def get_secret_key():
    global cached_api_key
    
    # すでに取得済みの場合はキャッシュを返す
    if cached_api_key:
        return cached_api_key

    try:
        # Parameter Storeから復号した状態で機密情報を取得
        response = ssm_client.get_parameter(
            Name=PARAMETER_NAME,
            WithDecryption=True  # 暗号化を解除して取得する指定
        )
        # 取得できた値をキャッシュに保持
        cached_api_key = response['Parameter']['Value']
        return cached_api_key

    except ClientError as e:
        # ログに秘密情報そのものは絶対に出力しない!
        print(f"パラメータの取得に失敗しました: {e.response['Error']['Code']}")
        raise e

def lambda_handler(event, context):
    # 関数の処理が始まったタイミングで貸金庫から鍵を取り出す
    api_key = get_secret_key()
    
    # 取得したキーを使って安全に外部APIなどと連携
    # ... ここに実際の業務ロジックを書く ...
    
    return {
        "statusCode": 200,
        "body": "安全に処理が完了しました!"
    }

コードの安全ポイント

1. 環境変数には「パラメータの置き場所の名前(キー名)」しか書かない
万が一環境変数が外部から見えても、実際のAPIキーは見えません。
2. WithDecryption=True を使う
保管時は暗号化されており、Lambdaが使う瞬間だけ安全に平文に戻します。
3. メモリキャッシュを活用する
Lambdaが続けて実行される際、無駄にParameter Storeへアクセスしないようにメモリ上で再利用します。Lambdaが終了すればメモリからも綺麗に消え去ります。

—

6. まとめ:一歩ずつセキュアな開発者へ

最後に、今回学んだ大切なポイントを振り返りましょう!

  • Lambdaの環境変数に直接APIキーやパスワードを書くのは「玄関マットの下の合鍵」と同じで危険!
  • 攻撃者はアプリの脆弱性や強すぎる権限を突いて、環境変数を覗き見ようとしてくる。
  • 機密情報は「Parameter Store」や「Secrets Manager」という貸金庫に預けるのが鉄則。
  • Lambdaには「最小限の取り出し権限」だけを与えて、必要な瞬間にだけ動的に鍵を取得する。

最初は「少しコードが増えて面倒だな……」と感じるかもしれません。
ですが、一度この安全なパターンを覚えてしまえば、どんなクラウドアプリケーションを作る際にも一生役に立つ強力な武器になります。

大切なシステムとユーザーのデータを守るために、ぜひ次回の開発からは「動的な機密情報管理」を取り入れてみてくださいね。一歩ずつ、一緒にセキュアなエンジニアを目指していきましょう!

コメント

タイトルとURLをコピーしました