3.0 KiB · text 5af55d9
defmodule GitGud.Workflows.RunnerJwt do
@moduledoc """
Mints + verifies short-lived JWTs for a runner working on a specific
job.
The runner gets one token per task it pulls. The token claims:
sub → "runner-task"
jti → "task:<job_id>"
tid → workflow_job.id
rid → workflow_run.id
pid → repository.id
exp → now + 24h (long enough for any reasonable job; tighten
after we have job-deadline metadata)
Used by:
* `Task.context["token"]` and `Task.context["gitea_runtime_token"]`
— what forgejo-runner injects as `GITHUB_TOKEN` and the
artifact/cache service runtime token
* `RunnerJwtAuth` plug for `/api/actions/_apis/artifacts/*` and
`/_apis/artifactcache/*` endpoints
Signed HS256 with the secret configured under:
config :git_gud, GitGud.Workflows.RunnerJwt,
secret: "<base64 32 bytes>",
ttl_seconds: 86400
HS256 because only we mint and verify; no need for asymmetric keys
the way federation does.
"""
alias GitGud.Workflows.WorkflowJob
alias GitGud.Workflows.WorkflowRun
@issuer "gitgud"
@audience "runner"
@doc "Mint a token for a job that's just been claimed."
def mint(%WorkflowJob{id: jid, workflow_run_id: rid}, %WorkflowRun{repository_id: pid}) do
now = System.system_time(:second)
ttl = ttl_seconds()
claims = %{
"iss" => @issuer,
"aud" => @audience,
"sub" => "runner-task",
"jti" => "task:#{jid}",
"tid" => jid,
"rid" => rid,
"pid" => pid,
"iat" => now,
"nbf" => now - 10,
"exp" => now + ttl
}
signer = signer()
{:ok, jwt, _} = Joken.encode_and_sign(claims, signer)
jwt
end
@doc """
Verify a token from a runner request. Returns `{:ok, claims}` or
`{:error, reason}`.
"""
def verify(token) when is_binary(token) do
case Joken.verify(token, signer()) do
{:ok, claims} ->
cond do
claims["iss"] != @issuer ->
{:error, :bad_issuer}
claims["aud"] != @audience ->
{:error, :bad_audience}
System.system_time(:second) > claims["exp"] ->
{:error, :expired}
true ->
{:ok, claims}
end
{:error, _} = err ->
err
end
end
def verify(_), do: {:error, :no_token}
defp signer do
secret = signing_secret()
Joken.Signer.create("HS256", secret)
end
defp signing_secret do
cfg = Application.get_env(:git_gud, __MODULE__, [])
case Keyword.fetch(cfg, :secret) do
{:ok, value} when is_binary(value) and value != "" ->
value
_ ->
raise """
no signing secret configured for #{inspect(__MODULE__)}.
Set in runtime.exs:
config :git_gud, GitGud.Workflows.RunnerJwt,
secret: System.fetch_env!("GITGUD_RUNNER_JWT_SECRET")
"""
end
end
defp ttl_seconds do
Application.get_env(:git_gud, __MODULE__, [])
|> Keyword.get(:ttl_seconds, 86_400)
end
end