セキュリティ

SENCA を安全に運用するための機能と設定です。管理 API の認証と権限、JWT、ストリームキー、署名付きの再生 URL、シークレットの暗号化、外部への接続の制限、クラスターの mTLS、コーデックワーカーのサンドボックスを説明します。

運用と安全

このページの見出し
  1. 管理 API の認証
  2. JWT
  3. ストリームキー(配信者の資格情報)
  4. 署名付きの再生 URL
  5. シークレット
  6. 外部への接続の制限(SSRF 対策)
  7. クラスターの mTLS
  8. データディレクトリの保護
  9. コーデックワーカーのサンドボックス
  10. ルームの参加トークン
  11. HTTPS・RTMPS
  12. 監査ログ
  13. 関連するページ

SENCA のノードをインターネットに公開する前に、一度通して読んでください。 誰がどの資格情報で何をできるか、シークレットをどう守っているか、ノードが外へ接続するときに何を制限しているかを、設定とあわせて説明します。

このページの要点:

  • 管理 API は Authorization: Bearer で認証します。senca_ で始まる値は API トークン、それ以外は JWT として扱います。操作できる範囲は、権限(スコープ)とテナントで決まります。
  • 最初の管理者トークンは強い権限を持つため、運用を始める前に権限を絞った期限付きのトークンに入れ替えてください。
  • 配信者はストリームキー、視聴者は期限付きの署名付き URL、ルームの参加者は参加トークンで認証します。
  • シークレット(配信先のストリームキーなど)は AES-256-GCM で暗号化して保存し、API からは読み出せません。ノードから外部への接続では、既定でプライベートアドレスなどへの接続を拒否します。
  • コーデックのライブラリは、権限を制限した別のプロセス(コーデックワーカー)の中でだけ動かします。

管理 API の認証

管理 API(/api/v1/...)と SSE(/api/v1/events)は、Authorization: Bearer <値> ヘッダーで認証します。

値が senca_ で始まれば API トークン、それ以外は JWT として扱います。JWT が使えるのは --jwks を指定したときだけです。

認証に失敗すると 401(WWW-Authenticate: Bearer)が返ります。未知のトークン、期限切れ、署名の誤りといった失敗の理由は、応答には含めません。 理由はノードのログに auth failed: <理由> request_id=... として残るので、そちらで調べてください。

認証なしで応答するのは、/healthz・/readyz、管理画面と配信用ページの静的ファイル、署名付きの再生 URL(/s/...)だけです。 WHIP はストリームキー、WHEP は署名、ルームのシグナリングは参加トークンと、それぞれ別の資格情報で認証します。

API トークン

API トークンは POST /api/v1/tokens で作ります。本文は {"name": "...", "scopes": ["..."], "expires_at": "RFC 3339"} で、expires_at は省略できますが、過去の時刻は指定できません。 トークンの値が返るのは、このときの応答(201)だけです。一覧や取得では返らないので、作ったらすぐに控えてください。

作成には tenant:admin が必要で、自分が持っている権限しか付与できません。できたトークンは、作成者と同じテナントに属します。 1 つのテナントで作れるトークンは 100 個までです。テナントで最後の tenant:admin トークンは削除できません(409)。

値の形式は、senca_ の後に 32 バイトの乱数を base64url で表したもの(43 文字)を続けたものです。サーバーには SHA-256 ハッシュだけを保存します。 照合では、比較にかかる時間から値を推測されないよう、すべてのトークンを一定の時間で比較します。

権限(スコープ)

権限できること
stream:readストリーム・予約・入力・出力・パイプライン・ルーム・メディアパイプラインの参照、静止画の取得、SSE の購読
stream:writeストリームの作成・変更・削除・鍵の再発行、録画の開始・停止・変更(保持期間・リーガルホールド)、予約・入力・出力・パイプライン・ルーム・メディアパイプラインの変更、ルームの参加トークンの発行
publishWHIP での配信(POST /whip/{stream_id} の Authorization: Bearer に、ストリームキーの代わりに使えます。同じテナントのストリームだけが対象です)
subscribeライブの再生 URL の発行
recording:read録画の参照、録画の再生 URL・サムネイル・MP4 のエクスポート
recording:delete録画の削除
tenant:adminnode:admin 以外のすべて。加えて、トークンとシークレットの管理
node:adminテナントをまたぐ運用操作(/nodes・/grid・/devices、ノードのドレイン、デバイスの停止・再開、ノード・グリッドのイベントの購読)

操作ごとに必要な権限は、管理 API と SDK、API リファレンス、 OpenAPI(docs/api/openapi.yaml)を見てください。

テナント

テナントは、同じノードを複数の利用者(部署や顧客)で分けて使うための区切りです。 すべてのリソースはテナントに属し、他のテナントのリソースは見えません(404 になります)。最初の管理者トークンのテナントは default です。 API トークンは作成者と同じテナントにしか作れないため、別のテナントを作るには JWT を使います(テナントは JWT のクレームで決まります)。

最初の管理者トークン

初回の起動時、ノードは data_dir/control/bootstrap-admin-token(パーミッション 0600)に管理者トークンを書き出します。 権限は tenant:admin と node:admin、期限はありません。クラスターでは、ID が最も小さいノードだけがこれを作ります。 何でもできて期限も無いトークンなので、運用を始める前に次の手順で入れ替えてください。

  1. 日々の操作に使う、必要な権限だけを持つ期限付きのトークンを作ります。
  2. 新しい管理者トークンを作り、それから最初のトークンを DELETE /api/v1/tokens/{id} で削除します。 最後の tenant:admin トークンは削除できないので、先に新しいものを作っておく必要があります。
  3. bootstrap-admin-token ファイルを削除するか、安全な場所へ移します。ノードがこのファイルを読み直すことはありません。

リクエストの数の上限

総当たりや使いすぎを防ぐため、ノードはリクエストの数を数え、上限を超えると 429(RATE_LIMITED)を返します。応答の Retry-After には次に 送ってよいまでの秒数が、details.limit にはどの上限にかかったかが入ります。上限はノードごとに数えます(クラスター全体の合計ではありません)。

details.limit数える単位対象既定(オプション)
ip送信元の IP アドレス管理 API(/api/v1)。認証の前に数えます1 秒に 300(--rate-limit-ip)
tokenAPI トークン(JWT は sub の値)管理 API1 秒に 200(--rate-limit-token)
auth_failures送信元の IP アドレス認証の失敗(管理 API・SSE・WHIP での誤ったトークンやストリームキー)1 分に 30(--rate-limit-auth-failures)
connect送信元の IP アドレス接続の開始(/signaling・WHIP・WHEP・SSE)1 秒に 30(--rate-limit-connect)

1 秒あたりの上限は、短い間なら 2 倍まで受け付けます。 認証の失敗が上限に達した IP アドレスからは、回数が回復するまで、正しいトークンを送っても 429 が返ります。

ここでいう送信元の IP アドレスは、ノードが受けた TCP 接続の相手です。リバースプロキシの背後に置くと全員が同じアドレスに見えるので、プロキシ側でも上限を設けてください。 どの上限も 0 で無効にできます(設定)。

JWT

JWT(JSON Web Token)は、外部の認証基盤(IdP)が発行する署名付きのトークンです。IdP のトークンをそのまま使うときは、署名を検証するための鍵を、ノード上の JWKS ファイルで指定します。

senca-node ... --jwks /etc/senca/jwks.json \
  --jwt-issuer https://idp.example.com/ --jwt-audience senca [--jwt-tenant-claim tenant]

使えるアルゴリズムは EdDSA(Ed25519)・ES256(P-256)・RS256(2048 ビット以上)です。アルゴリズムは鍵の種類で決まり、ヘッダーの alg が鍵と合わなければ拒否します。 none と HS 系は使えません。

JWKS の各鍵には kid が必要です。use を書くなら sig、alg を書くなら鍵の種類と一致させてください。 対応していない鍵が 1 つでもあると、黙って読み飛ばさずに起動を止めます。ファイルを読むのは起動時の 1 回だけなので、鍵を替えたら再起動が必要です。 トークンに書かれた URL を見て鍵を取りに行くこともありません。

トークンのヘッダーには kid が必須で、crit があれば拒否します。クレーム(トークンに書かれた項目)は次のように扱います。

クレーム扱い
exp・nbfexp は必須、nbf は任意。時計のずれは 60 秒まで許容します
iss・audiss が --jwt-issuer と一致し、aud(文字列または配列)に --jwt-audience が含まれている必要があります
テナント--jwt-tenant-claim で指定した名前のクレーム(既定は tenant)から取ります。空か、クレームが無ければ拒否します
scope・scp権限を scope(空白区切りの文字列または配列)から、無ければ scp から取ります。未知の権限名は無視します
sub監査ログの subject になります

トークンの中身(ペイロード)の例です。

{"iss": "https://idp.example.com/", "aud": "senca", "sub": "alice", "tenant": "acme",
 "scope": "stream:read stream:write", "exp": 1793669243}

この例のトークンでは、テナント acme にストリームを作成できます。tenant:admin が必要な API は 403 になり、 作成したストリームは default テナントからは見えません。

ストリームキー(配信者の資格情報)

RTMP・RTMPS・SRT・WHIP の配信者は、ストリームキーで認証します。 渡し方は、RTMP なら URL の最後の部分(rtmp://host:1935/live/<key>)、SRT なら streamid の publish:<key>、WHIP なら Authorization: Bearer <key> です。

キーの形式は、sk_ の後に 32 バイトの乱数を base64url で表したものを続けたものです。サーバーには SHA-256 ハッシュだけを保存し、 値が返るのはストリームの作成時と POST /api/v1/streams/{id}/keys(再発行)の応答だけです。再発行すると古いキーは使えなくなります。 ストリームキーもトークンも、ログ・エラー・デバッグ表示には出しません。

WHIP では、ストリームキーの代わりに publish の権限を持つ API トークン(または JWT)も使えます。対象は同じテナントのストリームだけです。 たとえば配信の機材にはストリームキーではなく期限付きのトークンを渡し、配信が終わったら削除する、という運用ができます。

予約に live があるストリームは、その時間帯にしか配信を受け付けません(録画)。

署名付きの再生 URL

視聴者には、期限付きの署名が入った再生 URL を渡します。 ライブの URL は POST /api/v1/streams/{id}/playback-url(subscribe)、録画(VOD)の URL は POST /api/v1/recordings/{id}/playback-url(recording:read)で発行します。 本文は {"ttl_secs": N} で、省略すると 300、指定できる範囲は 1〜--max-playback-ttl です。

ライブの署名は 1 つで、HLS(/s/{exp}/{sig}/live/{id}/...)、RTMP の再生(/play/{id}?exp=&sig=)、WHEP(/whep/{id}?exp=&sig=)、RTSP の再生のどれにも使えます。 録画は HLS(/s/{exp}/{sig}/vod/{id}/...)で再生します。期限(exp、UNIX 秒)を過ぎた URL や、署名が一致しない URL は 403 です。

URL を知っている人は誰でも、期限まで使えます。 視聴者ごとに紐付けたり、途中で取り消したりする機能はありません。期限は短くし、必要になるたびに発行してください。

署名は、{kind}/{id}/{exp}(kind は live か vod)の HMAC-SHA256 の先頭 16 バイトを base64url で表したものです。 鍵はコントロールプレーンに保存された再生用の鍵(32 バイトの乱数)で、初回に作られ、クラスターでは全ノードで共通です。 この鍵を入れ替える API は無く、コントロールプレーンのデータが漏れると URL を偽造できてしまいます(下の「データディレクトリの保護」)。

シークレット

入力・出力の資格情報(RTMP の鍵、SRT のパスフレーズ、PaaS の資格情報)と字幕の API キーは、シークレットとして登録し、参照名で使います。 URL に資格情報を書くこと(rtmp://user:pass@...)はできません。

curl -s -X POST http://127.0.0.1:8080/api/v1/secrets \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"youtube-key","value":"xxxx-xxxx"}'
# → {"id": "...", "name": "youtube-key", "ref": "secret:youtube-key", "created_at": "..."}

登録したシークレットは、入力・出力では credential_ref: "secret:{name}"、字幕では asr_credential_ref・translate_credential_ref で参照します。 シークレットの作成・一覧・削除には tenant:admin が必要です。登録した値は二度と返らず、入力・出力・字幕が使っているシークレットは削除できません(409)。

値は AES-256-GCM で暗号化し、暗号化した形だけをコントロールプレーンに保存します。暗号化のときにテナントと ID も結び付けるので、 暗号文を別のリソースへコピーしても復号できません。

単一ノードの鍵

単一ノードでは、ノードの data_dir/control/secret-key(32 バイト、パーミッション 0600。無ければ初回起動時に作成)を鍵に使います。 このファイルを失うと登録済みのシークレットは復元できず、登録し直すしかありません。

クラスターの鍵

クラスターでは、クラスター共通のデータ鍵を使います。データ鍵は、クラスターシークレット(secret_file)から HKDF-SHA256 で 作った鍵で AES-256-GCM により暗号化し、コントロールプレーンのデータに含めて全ノードへ複製します。データ鍵がまだ無い場合は、 各ノードが自分の secret-key を暗号化してクラスターに書き込もうとし、最初に適用されたものが使われます(後から置き換わることはありません)。 各ノードはコントロールプレーンからデータ鍵を復号するので、どのノードで登録したシークレットもすべてのノードで使えます。鍵ファイルを コピーする必要はありません。

クラスターシークレットは全ノードで同じにしてください。違うクラスターシークレットを持つノードはデータ鍵を復号できず、ログに cannot open the cluster-wide secret key と出して、シークレットを使えなくなります(登録は 503)。 データ鍵がまだ届いていないノードでシークレットを登録した場合も、503(retryable: true)になります。

コントロールプレーンのデータとクラスターシークレットがそろえば、シークレットは復元できます。逆に、バックアップや Raft のログといったコントロールプレーンのデータのコピーだけでは、クラスターシークレットを知らない限り復号できません。 クラスターシークレットには、openssl rand -hex 32 で作る 32 バイトの乱数のように、推測できない値を使ってください。 なお、クラスターシークレットを変えるとデータ鍵を復号できなくなります。クラスターシークレットのローテーションには対応していません。

以前のバージョンからの移行: 以前のバージョンのクラスターでは、登録を受け付けたノードの secret-key でシークレットを暗号化していました。 現在のバージョンは、復号のときにまずデータ鍵を試し、だめならノードの secret-key も試します。さらに起動時の初期化の後、ノードの鍵でしか 復号できないシークレットをデータ鍵で暗号化し直します(ログに re-sealed N secret(s))。古い値を暗号化したノードを 一度現在のバージョンで起動すれば、そのシークレットはすべてのノードで使えるようになります。

外部への接続の制限(SSRF 対策)

SSRF(Server-Side Request Forgery)は、サーバーをだまして内部ネットワークへ接続させる攻撃です。 SENCA のノードは、pull の入力、push の出力、HLS の取り込み、字幕の送信先、配信サービス(PaaS)の API、ルームの Webhook で、利用者が指定した宛先へ接続します。 そこで名前解決した後の IP アドレスを検査し、既定では次の宛先を拒否します。

  • ループバック(127.0.0.0/8・::1)、未指定アドレス(0.0.0.0・::)、0.0.0.0/8
  • リンクローカル(169.254.0.0/16。クラウドのメタデータの 169.254.169.254 を含みます。IPv6 では fe80::/10)
  • プライベートアドレス(10.0.0.0/8・172.16.0.0/12・192.168.0.0/16)、CGNAT(100.64.0.0/10)、ULA(fc00::/7)
  • マルチキャスト、ブロードキャスト。IPv4 射影 IPv6 アドレス(::ffff:a.b.c.d)は IPv4 として検査します

社内のカメラなどへ接続するときは、管理者が範囲を許可します。

senca-node ... --egress-allow 10.1.0.0/16 --egress-allow 192.168.10.0/24

拒否されたときは、入力・出力の status.last_error とログに <宛先> は許されていない宛先です(--egress-allow で許可が必要) と表示されます。 ノードはその後も、間隔を少しずつ延ばしながら再接続を試み続けます。

クラスターの mTLS

mTLS(相互 TLS)は、通信の両側が証明書で互いを確認する方式です。 クラスターの設定に tls_ca・tls_cert・tls_key を書くと、ノード間の通信(Raft と中継)が mTLS になります。 双方がクラスターの認証局で相手の証明書を検証し、名前 senca-cluster を確認します。

証明書は、たとえば次のように作ります(P-256)。

# 認証局(1 回だけ。鍵はノードに置かない)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout cluster-ca.key -out cluster-ca.pem -days 3650 -subj "/CN=senca-cluster-ca"

# ノードごと(n1, n2, n3 ...)
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout n1.key -out n1.csr -subj "/CN=n1"
printf "subjectAltName=DNS:senca-cluster\nbasicConstraints=CA:FALSE\nextendedKeyUsage=serverAuth,clientAuth\n" > n1.ext
openssl x509 -req -in n1.csr -CA cluster-ca.pem -CAkey cluster-ca.key -CAcreateserial \
  -out n1.pem -days 825 -extfile n1.ext

証明書の SAN には DNS:senca-cluster、拡張鍵用途には serverAuth と clientAuth の両方が必要です。 各ノードに cluster-ca.pem と自分の nN.pem・nN.key を置き、クラスターの設定で指定してください。別の認証局が発行した証明書を持つノードは拒否されます。 Raft と中継の接続では、TLS の内側でさらに、クラスターシークレット(secret_file)による相互のチャレンジレスポンス認証を行います(次項)。

クラスターシークレットによるノードの認証

Raft と中継の接続では、mTLS の有無にかかわらず、接続の直後に相互のチャレンジレスポンス認証を行います。

  1. 接続する側が、自分のノード番号と新しい 32 バイトの乱数を送ります。
  2. 受け付ける側が、自分のノード番号、自分の新しい乱数、HMAC-SHA256(K, "server" ‖ 用途 ‖ ノード番号 ‖ 双方の乱数) を返します。
  3. 接続する側は、相手が接続先のノードであることと HMAC の値が正しいことを確かめてから、HMAC-SHA256(K, "client" ‖ …) を送ります。 受け付ける側がそれを検証します。

K はクラスターシークレットから HKDF-SHA256 で作ります(Raft 用と中継用で別の鍵です)。クラスターシークレットそのものも、毎回同じになる値も、 ネットワークに流れません。また、どの HMAC も相手が選んだ乱数を含むため、記録したやり取りを再送しても認証は通りません。 接続する側は、相手を確かめてから自分の証明を送ります。メッセージは固定長で、5 秒以内に終わらなければ接続を閉じます。

平文(クラスターシークレットだけ)で使う場合

tls_* を設定しないと、ノード間は平文の TCP になり、相手の確認は上のチャレンジレスポンス認証だけになります。 コントロールプレーンの複製(トークンとストリームキーのハッシュ、再生用の鍵、暗号化したシークレットとシークレットの鍵など)も中継のメディアも、暗号化されずに流れます。 認証が済んだ後の通信を改ざんされても防げません。平文で動かすのは、外部から隔離された信頼できるネットワークの中だけにしてください。

データディレクトリの保護

単一ノードの control/log・control/snapshot.json と、クラスターの control/raft/ には、再生用の鍵(16 進の平文)、 トークンとストリームキーのハッシュ、暗号化したシークレットが入っています。再生用の鍵があれば、再生 URL もルームの参加トークンも偽造できます。

ノードは secret-key・bootstrap-admin-token をパーミッション 0600 で作り、起動のたびに control/ を 0700 にするので、制御用のファイルは他のユーザーから読めません。 ただし録画とセグメントのパーミッションは umask に従います。データディレクトリは専用のユーザーで運用し、chmod 700 にしてください。 Docker イメージは専用ユーザー senca で動き、データはボリュームに置きます。

バックアップにも同じ秘密情報が入っています。保存先も同じように守ってください(運用)。

コーデックワーカーのサンドボックス

サンドボックスは、プログラムができることを制限して、万一のときの被害を小さくする仕組みです。 外部のコーデックライブラリ(GPU の SDK・dav1d・libvpx など)はノードのプロセスには読み込まず、 別のプロセス(コーデックワーカー)で動かします。コーデックワーカーは、起動時に次の制限を自分にかけます(Linux)。

段階かける制限
ライブラリを読み込む前標準出力の隔離(ノードとの通信に使うファイル記述子を分ける)、PR_SET_PDEATHSIG=SIGKILL、PR_SET_DUMPABLE=0、RLIMIT_CORE=0、RLIMIT_NOFILE=256、PR_SET_NO_NEW_PRIVS、Landlock(対応するカーネルの場合。/dev の下以外でのファイルの作成・書き込み・削除を禁止し、読み取りは制限しない)
ライブラリを読み込んだ後seccomp-BPF の拒否リスト(全スレッド)。ネットワーク(socket・connect・bind・listen・accept)、プログラムの実行、ptrace・他プロセスのメモリ、mount、カーネルモジュール、kexec、bpf、キーリング、名前空間、reboot・swap・時計の設定などを禁止します。clone3 は ENOSYS を返します
ジョブごとRLIMIT_CPU(1 ジョブあたり 300 秒)。経過時間のタイムアウトはノードが監視します(単発のジョブは 30 秒)

コーデックワーカーは環境変数を空にして起動し、渡すのは SENCA_CODEC_LIB_DIR・SENCA_CODEC_DISABLE・LD_LIBRARY_PATH だけです。 実際にかかった制限は起動時のログに出ます(例: sandbox: core=0; nofile=256; no_new_privs; landlock(abi 8): writes only under /dev; cpu=300s/job; seccomp deny-list)。 GET /api/v1/devices(node:admin)の sandbox でも確認できます。

コーデックワーカーが異常終了したり、応答しなくなったり、不正な出力を返したりしても、失敗するのはそのジョブだけです。コーデックワーカーは起動し直され、ノードは止まりません。

既定ではメモリの上限(RLIMIT_AS)はかけません。また、ネットワークの利用(socket)を禁止しているため、GPU ドライバーの補助機能の一部 (NVIDIA の persistenced への接続など)は使えません。

ルームの参加トークン

ルームの参加トークンは、POST /api/v1/rooms/{id}/tokens(stream:write)で発行する JWT 互換のトークン(EdDSA)です。 iss は senca、aud は senca-rooms、kid は senca-rooms-1 で、中にはルーム・役割・client_id・bundle_id・メタデータ・権限が入ります。 有効期間は ttl_secs(既定 600、範囲 10〜86400)で決めます。まだ無いルームを指定すると、そのルームが自動で作られます。

署名鍵は、再生用の鍵から作った Ed25519 の鍵(SHA-256("senca-rooms-join-token/1\0" || 再生用の鍵))です。 全ノードで共通なのでどのノードでも発行・検証できますが、再生用の鍵が漏れれば参加トークンも偽造できます。

既定では、ルームに参加するには参加トークンが必須です。認証 Webhook(auth_webhook_url・--rooms-auth-webhook)を設定すると、 トークンを検証した後に Webhook にも問い合わせます。--rooms-auth-mode only のときは、トークンの代わりに Webhook が参加を認めます。 詳しくは リアルタイムのルーム を見てください。

HTTPS・RTMPS

--https・--rtmps と --tls-cert・--tls-key(PEM)を指定すると、TLS 1.3 で接続を受け付けます。鍵は ECDSA P-256・RSA・Ed25519 に対応しています。 TLS のプロトコル処理は SENCA 自身の実装で、暗号の演算には検証済みのライブラリを使っています。 HLS や WHEP など配信量の多い経路を TLS にしたいときは、前段のリバースプロキシで TLS を終端してもかまいません。署名はパスと クエリに含まれるので、プロキシを通しても検証できます。公開する URL は --public-http-url で合わせてください。

監査ログ

data_dir/audit.log に、状態を変更する管理 API の操作を 1 行 1 JSON で追記します。

{"action":"stream.create","request_id":"req_...","resource":"<リソース ID>","subject":"token:<トークン ID>","tenant":"default","ts":"2026-11-03T01:10:15.531Z"}

subject は、API トークンなら token:{ID}、JWT なら sub の値です。秘密情報やリクエストの本文は記録しません。 Idempotency-Key で同じ応答を返すために保存しておく応答からも、stream_key・token・secret を消しています。 そのため再送したときの応答では、これらが null になり、*_redacted: true が付きます。

関連するページ

この文書の原文は、リポジトリの docs/guide/ja/12-security.md にあります。内容は現在の版(v0.0.1)に合わせています。