mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-14 03:01:48 +00:00
Add DNS allowlist authorization.
Also, improve authorization docs and tighten rules for `INSECURE`.
This commit is contained in:
14
README.md
14
README.md
@@ -53,11 +53,17 @@ $ curl http://127.0.0.1:3333/ -H 'Host: codeberg.page'
|
||||
Authorization
|
||||
-------------
|
||||
|
||||
DNS is used for authorization of content updates.
|
||||
DNS is used for authorization of content updates, either via TXT records or by pattern matching. The authorization flow proceeds sequentially in the following order, with the first of multiple applicable rule taking precedence:
|
||||
|
||||
- If a `[wildcard]` configuration section is specified, and if the suffix of a hostname in a `POST` request is equal to `[wildcard].domain`, then the request is authorized when and only when the repository URL in the event body matches the repository URL computed from the configuration file. Otherwise the next rule is used.
|
||||
- If a `PUT` or `POST` request is received at `<hostname>` with an `Authorization: Pages <token>` header (or, in absence of such, with an `Authorization: Basic <basic>` header, where `<basic>` is equal to `Base64("Pages <token>")`), then the request is authorized when any of the the TXT records at `_git-pages-challenge.<hostname>` are equal to `SHA256("<hostname> <token>")`.
|
||||
- During development, set environment variable `INSECURE=1` to bypass this checks.
|
||||
1. **Development Mode:** If the environment variable `INSECURE` is set to the value `very`, the request is authorized to update from any clone URL.
|
||||
2. **DNS Challenge:** If the method is `PUT` or `POST`, and a well-formed `Authorization:` header is provided containing a `<token>`, and a TXT record lookup at `_git-pages-challenge.<hostname>` returns a record whose concatenated value equals `SHA256("<hostname> <token>")`, the request is authorized to update from any clone URL.
|
||||
- **<code>Pages</code> scheme:** Request includes an `Authorization: Pages <token>` header.
|
||||
- **<code>Basic</code> scheme:** Request includes an `Authorization: Basic <basic>` header, where `<basic>` is equal to `Base64("Pages:<token>")`. (Useful for non-Forgejo forges.)
|
||||
3. **DNS Allowlist:** If the method is `PUT` or `POST`, and a TXT record lookup at `_git-pages-repository.<hostname>` returns a set of well-formed absolute URLs, the request is authorized to update from clone URLs in the set.
|
||||
4. **Wildcard Match:** If the method is `POST`, and a `[wildcard]` configuration section is present, and the suffix of a hostname (compared label-wise) is equal to `[wildcard].domain`, the request is authorized to update from a *matching* clone URL.
|
||||
- **Index repository:** If the request URL is `scheme://<hostname>/`, a *matching* clone URL is computed as `sprintf([wildcard].clone-url, <hostname>, [wildcard].index-repo)`.
|
||||
- **Project repository:** If the request URL is `scheme://<hostname>/<projectName>/`, a *matching* clone URL is computed as `sprintf([wildcard].clone-url, <hostname>, <projectName>)`.
|
||||
5. **Default Deny:** Otherwise, the request is not authorized.
|
||||
|
||||
|
||||
Architecture (v2)
|
||||
|
||||
53
src/auth.go
53
src/auth.go
@@ -8,6 +8,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -98,13 +99,13 @@ func authorizeDNSChallenge(r *http.Request) ([]string, error) {
|
||||
actualChallenges, err := net.LookupTXT(challengeHostname)
|
||||
if err != nil {
|
||||
return nil, AuthError{http.StatusUnauthorized,
|
||||
fmt.Sprintf("failed to look up DNS challenge: TXT %s", challengeHostname)}
|
||||
fmt.Sprintf("failed to look up DNS challenge: %s TXT", challengeHostname)}
|
||||
}
|
||||
|
||||
expectedChallenge := fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%s %s", host, param)))
|
||||
if !slices.Contains(actualChallenges, expectedChallenge) {
|
||||
return nil, AuthError{http.StatusUnauthorized, fmt.Sprintf(
|
||||
"defeated by DNS challenge: TXT %s %v does not include %s",
|
||||
"defeated by DNS challenge: %s TXT %v does not include %s",
|
||||
challengeHostname,
|
||||
actualChallenges,
|
||||
expectedChallenge,
|
||||
@@ -114,7 +115,30 @@ func authorizeDNSChallenge(r *http.Request) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func authorizeWildcardDomain(r *http.Request) ([]string, error) {
|
||||
func authorizeDNSAllowlist(r *http.Request) ([]string, error) {
|
||||
host := GetHost(r)
|
||||
|
||||
allowlistHostname := fmt.Sprintf("_git-pages-repository.%s", host)
|
||||
repoURLs, err := net.LookupTXT(allowlistHostname)
|
||||
if err != nil {
|
||||
return nil, AuthError{http.StatusUnauthorized,
|
||||
fmt.Sprintf("failed to look up DNS repository allowlist: %s TXT", allowlistHostname)}
|
||||
}
|
||||
|
||||
for _, repoURL := range repoURLs {
|
||||
if parsedURL, err := url.Parse(repoURL); err != nil {
|
||||
return nil, AuthError{http.StatusBadRequest,
|
||||
fmt.Sprintf("failed to parse URL: %s TXT %q", allowlistHostname, repoURL)}
|
||||
} else if !parsedURL.IsAbs() {
|
||||
return nil, AuthError{http.StatusBadRequest,
|
||||
fmt.Sprintf("repository URL is not absolute: %s TXT %q", allowlistHostname, repoURL)}
|
||||
}
|
||||
}
|
||||
|
||||
return repoURLs, err
|
||||
}
|
||||
|
||||
func authorizeWildcardMatch(r *http.Request) ([]string, error) {
|
||||
host := GetHost(r)
|
||||
hostParts := strings.Split(host, ".")
|
||||
|
||||
@@ -139,12 +163,13 @@ func authorizeWildcardDomain(r *http.Request) ([]string, error) {
|
||||
}
|
||||
|
||||
// Returns `repoURLs, err` where if `err == nil` then the request is authorized to clone from
|
||||
// any repository URL exactly included in `repoURLs`, or any URL at all if `repoURLs == nil`.
|
||||
// any repository URL included in `repoURLs` (by case-insensitive comparison), or any URL at all
|
||||
// if `repoURLs == nil`.
|
||||
func authorizeRequest(r *http.Request, allowWildcard bool) ([]string, error) {
|
||||
causes := []error{AuthError{http.StatusUnauthorized, "unauthorized"}}
|
||||
|
||||
if os.Getenv("INSECURE") != "" {
|
||||
log.Println("auth ok: INSECURE mode")
|
||||
if os.Getenv("INSECURE") == "very" {
|
||||
log.Println("auth: INSECURE mode: allow any")
|
||||
return nil, nil // for testing only
|
||||
}
|
||||
|
||||
@@ -154,18 +179,28 @@ func authorizeRequest(r *http.Request, allowWildcard bool) ([]string, error) {
|
||||
} else if err != nil { // bad request
|
||||
return nil, err
|
||||
} else {
|
||||
log.Println("auth ok: DNS challenge")
|
||||
log.Println("auth: DNS challenge: allow any")
|
||||
return repoURLs, nil
|
||||
}
|
||||
|
||||
repoURLs, err = authorizeDNSAllowlist(r)
|
||||
if err != nil && IsUnauthorized(err) {
|
||||
causes = append(causes, err)
|
||||
} else if err != nil { // bad request
|
||||
return nil, err
|
||||
} else {
|
||||
log.Printf("auth: DNS allowlist: allow %v\n", repoURLs)
|
||||
return repoURLs, nil
|
||||
}
|
||||
|
||||
if allowWildcard {
|
||||
repoURLs, err = authorizeWildcardDomain(r)
|
||||
repoURLs, err = authorizeWildcardMatch(r)
|
||||
if err != nil && IsUnauthorized(err) {
|
||||
causes = append(causes, err)
|
||||
} else if err != nil { // bad request
|
||||
return nil, err
|
||||
} else {
|
||||
log.Println("auth ok: wildcard *.%s: allow %v", config.Wildcard.Domain, repoURLs)
|
||||
log.Printf("auth: wildcard *.%s: allow %v\n", config.Wildcard.Domain, repoURLs)
|
||||
return repoURLs, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user