defmodule GitGudWeb.Plugs.HookAuth do
@moduledoc """
Guards the endpoints only this node's own git hooks and SSH shim may
call: `/_hooks/pre-receive`, `/_hooks/post-receive` and `/_ssh/resolve`.
They used to be guarded by nothing but a comment saying "bind to
127.0.0.1", while the endpoint listens on every interface. Anyone could
then ask `/_ssh/resolve` which key may push where, or post a fake
post-receive naming any pusher and start workflows as them.
A request must carry `x-gitgud-hook-token` equal to a secret this node
holds. `put_secret/0` makes it at boot (or takes `GITGUD_HOOK_SECRET`
when the operator sets one) and exports it into the node's own OS
environment, so every `git` it spawns — `git http-backend` for smart
HTTP, `git-receive-pack` for the embedded SSH daemon — hands it down
to the hooks, the way `GITGUD_PUSHER_ID` already travels. The scripts
in `priv/scripts` send it.
A secret rather than a loopback-address check: behind a reverse proxy
on the same host (the usual way to terminate TLS for the .deb install)
or a mesh sidecar, every outside request arrives from 127.0.0.1 too,
so the peer address proves nothing; and a hook pointed at the node by
name through `GITGUD_HOOK_URL` would stop working.
The OpenSSH `authorized_keys` shim (`gitgud-ssh`) runs under sshd, not
under this node, so with that setup set `GITGUD_HOOK_SECRET` to the
same value in both environments.
"""
@behaviour Plug
import Plug.Conn
require Logger
@env "GITGUD_HOOK_SECRET"
@header "x-gitgud-hook-token"
@key {__MODULE__, :secret}
@doc "The header the hook scripts send the secret in."
def header, do: @header
@doc """
Settle this node's secret and export it to the OS environment the
node's children inherit. Called once from `GitGud.Application.start/2`.
"""
def put_secret do
secret =
case System.get_env(@env) do
s when is_binary(s) and byte_size(s) >= 16 -> s
_ -> 32 |> :crypto.strong_rand_bytes() |> Base.url_encode64(padding: false)
end
System.put_env(@env, secret)
:persistent_term.put(@key, secret)
:ok
end
@doc "This node's secret."
def secret do
case :persistent_term.get(@key, nil) do
nil ->
:ok = put_secret()
:persistent_term.get(@key)
s ->
s
end
end
@impl true
def init(opts), do: opts
@impl true
def call(conn, _opts) do
case get_req_header(conn, @header) do
[token] when is_binary(token) ->
if Plug.Crypto.secure_compare(token, secret()), do: conn, else: deny(conn)
_ ->
deny(conn)
end
end
defp deny(conn) do
Logger.warning(
"refused #{conn.method} #{conn.request_path} from #{:inet.ntoa(conn.remote_ip)}: " <>
"no valid #{@header}"
)
conn
|> put_resp_content_type("text/plain")
|> send_resp(403, "DENY forbidden\n")
|> halt()
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
2.9 KiB · text
History
fe05dfe