運用と安全
このページの見出し
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 | ストリームの作成・変更・削除・鍵の再発行、録画の開始・停止・変更(保持期間・リーガルホールド)、予約・入力・出力・パイプライン・ルーム・メディアパイプラインの変更、ルームの参加トークンの発行 |
publish | WHIP での配信(POST /whip/{stream_id} の Authorization: Bearer に、ストリームキーの代わりに使えます。同じテナントのストリームだけが対象です) |
subscribe | ライブの再生 URL の発行 |
recording:read | 録画の参照、録画の再生 URL・サムネイル・MP4 のエクスポート |
recording:delete | 録画の削除 |
tenant:admin | node: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 が最も小さいノードだけがこれを作ります。
何でもできて期限も無いトークンなので、運用を始める前に次の手順で入れ替えてください。
- 日々の操作に使う、必要な権限だけを持つ期限付きのトークンを作ります。
- 新しい管理者トークンを作り、それから最初のトークンを
DELETE /api/v1/tokens/{id}で削除します。 最後のtenant:adminトークンは削除できないので、先に新しいものを作っておく必要があります。 bootstrap-admin-tokenファイルを削除するか、安全な場所へ移します。ノードがこのファイルを読み直すことはありません。
リクエストの数の上限
総当たりや使いすぎを防ぐため、ノードはリクエストの数を数え、上限を超えると 429(RATE_LIMITED)を返します。応答の Retry-After には次に
送ってよいまでの秒数が、details.limit にはどの上限にかかったかが入ります。上限はノードごとに数えます(クラスター全体の合計ではありません)。
details.limit | 数える単位 | 対象 | 既定(オプション) |
|---|---|---|---|
ip | 送信元の IP アドレス | 管理 API(/api/v1)。認証の前に数えます | 1 秒に 300(--rate-limit-ip) |
token | API トークン(JWT は sub の値) | 管理 API | 1 秒に 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・nbf | exp は必須、nbf は任意。時計のずれは 60 秒まで許容します |
iss・aud | iss が --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 の有無にかかわらず、接続の直後に相互のチャレンジレスポンス認証を行います。
- 接続する側が、自分のノード番号と新しい 32 バイトの乱数を送ります。
- 受け付ける側が、自分のノード番号、自分の新しい乱数、
HMAC-SHA256(K, "server" ‖ 用途 ‖ ノード番号 ‖ 双方の乱数)を返します。 - 接続する側は、相手が接続先のノードであることと 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 が付きます。
関連するページ
- 設定・運用・管理 API と SDK
- クラスターとグリッド・リアルタイムのルーム
- セキュリティの考え方の概要: セキュリティ(紹介ページ)
- 脆弱性の報告: SECURITY.md
この文書の原文は、リポジトリの docs/guide/ja/12-security.md にあります。内容は現在の版(v0.0.1)に合わせています。