ENECHANGE Developer Blog

ENECHANGE開発者ブログ

ChatGPT botの安全性を高めるために工夫したこと

CTO室の岩本 (iwamot) です。

4月7日のプレスリリースでご案内の通り、ENECHANGEでは、ChatGPTを活用した業務効率化を進めています。

私も寄与したく、SlackへのChatGPT bot導入を提案し、実際に導入しました。結果、70名以上に利用されており、多少は貢献できたと思っています。

導入に際しては、安全性を高めるため、いくつか手を打ちました。具体的には「入力内容の保全・機密情報のマスキング・Azure OpenAI Serviceの利用」です。

以下、詳しくご紹介します。

入力内容の保全

まず、入力内容の保全についてです。何らかの事故が起こった場合、後から入力内容の精査が求められるかもしれません。そのため保全が必要です。

私が導入したChatGPT in SlackのDEBUGログには、ユーザの入力内容が含まれています。そこでFluent Bitを使ってログをフィルタリング・加工し、CloudWatch LogsとS3に出力する仕組みにしました。

Fluent Bitの設定は下記の通りです。ECSのFireLensで使う前提としています。

/fluent-bit/etc/parsers_chatgpt.conf

[PARSER]
    Name chatgpt_post_details
    Format regex
    Regex ^DEBUG:openai:api_version=\S+ data='(?<data>.*)' message='Post details'$
    Decode_Field_as escaped_utf8 data do_next
    Decode_Field_as json data

/fluent-bit/etc/chatgpt.conf(後述のマスキングに関わる部分は省略)

[SERVICE]
    Parsers_File parsers_chatgpt.conf
    # https://aws.amazon.com/jp/blogs/news/under-the-hood-firelens-for-amazon-ecs-tasks/
    Flush 1
    Grace 30

# OpenAI APIへのPOSTログのみに絞り込み
[FILTER]
    Name rewrite_tag
    Match app-firelens-*
    Rule $log ^DEBUG:openai:api_version=\S+\sdata='.*'\smessage='Post\sdetails'$ chatgpt.post_details.$TAG true

# dataの内容をUTF-8デコードしてJSONに変換
[FILTER]
    Name parser
    Match chatgpt.post_details.*
    Key_Name log
    Parser chatgpt_post_details
    Reserve_Data On

# ユーザ入力のみに絞り込み
[FILTER]
    Name grep
    Match chatgpt.post_details.*
    Exclude data['user'] system

# data JSONの内容をlift
[FILTER]
    Name nest
    Match chatgpt.post_details.*
    Operation lift
    Nested_under data

[OUTPUT]
    Name cloudwatch_logs
    Match chatgpt.post_details.*
    region ${AWS_DEFAULT_REGION}
    log_group_name ${POST_DETAILS_LOG_GROUP_NAME}
    log_stream_template $TAG[2]
    log_stream_prefix fallback-stream
    Retry_Limit 5

[OUTPUT]
    Name kinesis_firehose
    Match chatgpt.post_details.*
    region ${AWS_DEFAULT_REGION}
    delivery_stream ${POST_DETAILS_DELIVERY_STREAM}
    time_key date
    Retry_Limit 5

機密情報のマスキング

次に、機密情報のマスキングについてです。入力内容にメールアドレス・電話番号・クレジットカード番号が含まれる場合、漏洩事故を防ぐため、マスキングしてからOpenAI APIにPOSTするのが安全です。

ChatGPT in Slackにはマスキングの機能があるため、環境変数を指定するだけで実現できます。

私はECSのタスク定義で下記のように指定しました。

                {
                    "name": "REDACTION_ENABLED",
                    "value": "true"
                },
                {
                    "name": "REDACT_CREDIT_CARD_PATTERN",
                    "value": "(?<![0-9-])((?:\\d{4}(?:[ -]\\d{4}){3})|(?:\\d{4}[ -]\\d{6}[ -]\\d{4,5}))(?![0-9-])"
                },
                {
                    "name": "REDACT_EMAIL_PATTERN",
                    "value": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
                },
                {
                    "name": "REDACT_PHONE_PATTERN",
                    "value": "(?<!\\d)\\d{1,5}-\\d{1,4}-\\d{4}(?!\\d)"
                },
                {
                    "name": "REDACT_SSN_PATTERN",
                    "value": "(?!)"
                },

また、マスキングを検知してSlackに通知したく、Fluent Bitに下記の設定も加えています。

/fluent-bit/etc/filters_chatgpt.lua

function add_redaction_fields(tag, timestamp, record)
  local system_slack_id = string.match(record["messages"][1]["content"], "<@(U[%w]-)>")

  local new_record = record
  new_record["redacted_contents"] = {}

  for _, message in ipairs(record["messages"]) do
    local content = message["content"]
    if string.find(content, "^<@" .. system_slack_id .. ">: ") == nil then
      if string.find(content, "%[EMAIL%]") ~= nil or string.find(content, "%[PHONE%]") ~= nil or string.find(content, "%[CREDIT CARD%]") then
        content = string.gsub(content, "<@(U[%w]-)>", "<%1>")
        table.insert(new_record["redacted_contents"], content)
      end
    end
  end

  if #new_record["redacted_contents"] > 0 then
    new_record["redacted"] = "true"
  end

  return 2, timestamp, new_record
end

/fluent-bit/etc/chatgpt.conf(マスキングに関わる部分のみ抜粋)

# マスキング情報を追加
[FILTER]
    Name lua
    Match chatgpt.post_details.*
    script filters_chatgpt.lua
    call add_redaction_fields

# マスキングされたログに別のタグを付与
[FILTER]
    Name rewrite_tag
    Match chatgpt.post_details.*
    Rule $redacted true chatgpt.redacted.$TAG[2] true

# マスキング情報を削除
[FILTER]
    Name record_modifier
    Match chatgpt.post_details.*
    Remove_key redacted
    Remove_key redacted_contents

# マスキングされたメッセージのみ出力
[FILTER]
    Name record_modifier
    Match chatgpt.redacted.*
    Allowlist_key redacted_contents

[OUTPUT]
    Name slack
    Match chatgpt.redacted.*
    webhook ${REDACTED_WEBHOOK}

Azure OpenAI Serviceの利用

最後に、Azure OpenAI Serviceの利用についてです。Azure OpenAI Serviceはアクセス元を制限できるため、APIキーの漏洩リスクが軽減できます。OpenAI APIにはないメリットです。

ChatGPT in SlackはAzure OpenAI Serviceを未サポートだったため、サポートできるようpull requestを作成したところ、マージしていただけました。Azure OpenAI Serviceで必要となるdeployment_id等をopenai.Completion.createのパラメータに追加し、環境変数で指定できるようにしたものです。

github.com

その後、ECSのタスク定義に下記の環境変数を追加し、gpt-3.5-turbo版のエンドポイントをAzure OpenAI Serviceに変更済みです。gpt-4版は利用申請が受理され次第、変更したいと思っています。

                {
                    "name": "OPENAI_API_TYPE",
                    "value": "azure"
                }
                {
                    "name": "OPENAI_API_BASE",
                    "value": "https://enechange-chatgpt-in-slack.openai.azure.com"
                },
                {
                    "name": "OPENAI_API_VERSION",
                    "value": "2023-05-15"
                },
                {
                    "name": "OPENAI_DEPLOYMENT_ID",
                    "value": "example-deployment-id"
                },

さらに、AWS側のIPアドレスをNATゲートウェイで固定し、当該IPアドレスからのアクセスのみをAzure側で許可することで、APIキーの漏洩リスクを軽減できました。

まとめ

以上の工夫により、運用の手軽さは損なわず、ChatGPT botの安全性を高められました。

日進月歩の状況なので、導入したbotがいつまで使われるのか分からないのが正直なところですが、現時点では適切な手が打てたのではないかと思っています。