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)
warn_fail_open()
:ok
end
# The hooks inherit this node's environment, so an operator who set
# the pre-receive escape hatch here set it for every push. Say so at
# boot: it's meant for an emergency, not to be forgotten in a unit file.
defp warn_fail_open do
if System.get_env("GITGUD_PREHOOK_FAIL_OPEN") == "1" do
Logger.warning(
"GITGUD_PREHOOK_FAIL_OPEN=1: pushes are let through WITHOUT branch protection " <>
"whenever the pre-receive backend is unreachable or failing (5xx). " <>
"Unset it once the emergency is over."
)
end
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.
3.4 KiB · text
History
b868907