added webhook validation

This commit is contained in:
ston1th 2019-12-14 14:48:41 +01:00
commit dd88498e0d
6 changed files with 158 additions and 21 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
env.txt env.txt
secret.yaml secret.yaml
webhook-secret.yaml

View file

@ -10,6 +10,8 @@ This way we can trigger cleanups when the PR is closed.
``` ```
X-Interceptor-Push-Ref: string: "refs/heads/master" X-Interceptor-Push-Ref: string: "refs/heads/master"
X-Interceptor-Include-Repo: []string{"my/repo"}
X-Interceptor-Exclude-Repo: []string{"my/repo"}
``` ```
Response Headers: Response Headers:
@ -53,10 +55,33 @@ Issue: <Pull-Request ID>
Commit: <Pull-Request Head Commit Hash> Commit: <Pull-Request Head Commit Hash>
``` ```
## Validate Webhooks
Add secret webhook keys you want to have validated.
**NOTE:** the name must be `webhook-secret`.
```
cat <<EOF>webhook-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: webhook-secret
namespace: webhook-interceptor
type: Opaque
stringData:
github.com: "<secreet>"
github.com_foo: "<foo secret>"
github.com_foo_bar: "<bar secret>"
EOF
```
# Build and run # Build and run
Create git authentication secret: Create git authentication secret:
**NOTE:** the name must be `git-auth`.
``` ```
cat <<EOF>secret.yaml cat <<EOF>secret.yaml
apiVersion: v1 apiVersion: v1
@ -67,6 +92,8 @@ metadata:
type: Opaque type: Opaque
stringData: stringData:
github.com: "<username>:<password>" github.com: "<username>:<password>"
github.com_foo: "<foo username>:<foo password>"
github.com_foo_bar: "<bar username>:<bar password>"
EOF EOF
``` ```

View file

@ -4,5 +4,6 @@ source env.txt
kubectl -n webhook-interceptor delete deploy webhook-interceptor kubectl -n webhook-interceptor delete deploy webhook-interceptor
ko apply -f config/ ko apply -f config/
kubectl apply -f secret.yaml kubectl apply -f secret.yaml
kubectl apply -f webhook-secret.yaml
sleep 10 sleep 10
kubectl -n webhook-interceptor logs -f $(kubectl -n webhook-interceptor get pod -ojsonpath='{.items[0].metadata.name}') kubectl -n webhook-interceptor logs -f $(kubectl -n webhook-interceptor get pod -ojsonpath='{.items[0].metadata.name}')

100
git.go
View file

@ -4,9 +4,14 @@ package main
import ( import (
"bytes" "bytes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"hash"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -19,7 +24,10 @@ const (
contentType = "Content-Type" contentType = "Content-Type"
jsonType = "application/json" jsonType = "application/json"
secretName = "git-auth" authSecretName = "git-auth"
validateSecretName = "webhook-secret"
secretDelim = "_"
) )
type Git struct { type Git struct {
@ -27,17 +35,82 @@ type Git struct {
cl *http.Client cl *http.Client
} }
func (g *Git) getSecret(host string) (username, password string, err error) { func makeKeys(host, owner, repo string) []string {
sec, err := g.sec.Get(secretName, metav1.GetOptions{}) return []string{
host + secretDelim + owner + secretDelim + repo,
host + secretDelim + owner,
host,
}
}
func (g *Git) getValidateSecret(host, owner, repo string) (key []byte, ok bool) {
sec, err := g.sec.Get(validateSecretName, metav1.GetOptions{})
if err != nil { if err != nil {
return return
} }
s, ok := sec.Data[host] for _, v := range makeKeys(host, owner, repo) {
key, ok = sec.Data[v]
if ok {
break
}
}
return
}
func (g *Git) validate(r *http.Request, payload []byte, repo Repository) (valid bool) {
var (
h hash.Hash
sig []byte
err error
)
u, err := url.Parse(repo.CloneURL)
if err != nil {
return
}
key, ok := g.getValidateSecret(u.Host, repo.Owner.Login, repo.Name)
if !ok {
return
}
api := detectAPI(r)
switch api {
case giteaAPI:
sig, err = hex.DecodeString(r.Header.Get("X-Gitea-Signature"))
if err != nil {
return
}
h = hmac.New(sha256.New, key)
case githubAPI:
sig, err = hex.DecodeString(strings.TrimPrefix(r.Header.Get("X-Hub-Signature"), "sha1="))
if err != nil {
return
}
h = hmac.New(sha1.New, key)
}
h.Write(payload)
return hmac.Equal(h.Sum(nil), sig)
}
func (g *Git) getAuthSecret(host, owner, repo string) (username, password string, err error) {
sec, err := g.sec.Get(authSecretName, metav1.GetOptions{})
if err != nil {
return
}
var (
secret []byte
ok bool
)
for _, v := range makeKeys(host, owner, repo) {
secret, ok = sec.Data[v]
if ok {
break
}
}
if !ok { if !ok {
err = fmt.Errorf("no key found for host %s in secret %s/%s", host, sec.Namespace, sec.Name) err = fmt.Errorf("no key found for host %s in secret %s/%s", host, sec.Namespace, sec.Name)
return return
} }
a := strings.SplitN(string(s), ":", 2) a := strings.SplitN(string(secret), ":", 2)
if len(a) < 2 { if len(a) < 2 {
err = fmt.Errorf("missing credentials for host %s in secret %s/%s", host, sec.Namespace, sec.Name) err = fmt.Errorf("missing credentials for host %s in secret %s/%s", host, sec.Namespace, sec.Name)
return return
@ -47,8 +120,8 @@ func (g *Git) getSecret(host string) (username, password string, err error) {
return return
} }
func (g *Git) send(req *http.Request) (*http.Response, error) { func (g *Git) send(req *http.Request, owner, repo string) (*http.Response, error) {
user, pass, err := g.getSecret(req.URL.Host) user, pass, err := g.getAuthSecret(req.URL.Host, owner, repo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -69,7 +142,7 @@ func (g *Git) getPullRequest(repo Repository, api, id string) (pr PullRequest, e
if err != nil { if err != nil {
return return
} }
resp, err := g.send(req) resp, err := g.send(req, repo.Owner.Login, repo.Name)
if err != nil { if err != nil {
return return
} }
@ -97,7 +170,7 @@ func (g *Git) addLabel(repo Repository, api, label, color string) (labelID int64
return return
} }
resp, err := g.send(req) resp, err := g.send(req, repo.Owner.Login, repo.Name)
if err != nil { if err != nil {
err = fmt.Errorf("error getting labels of %s: %s", u, err) err = fmt.Errorf("error getting labels of %s: %s", u, err)
return return
@ -121,6 +194,9 @@ func (g *Git) addLabel(repo Repository, api, label, color string) (labelID int64
return return
} }
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
if api == githubAPI {
color = strings.TrimPrefix(color, "#")
}
l := &Label{Name: label, Color: color} l := &Label{Name: label, Color: color}
err = json.NewEncoder(buf).Encode(l) err = json.NewEncoder(buf).Encode(l)
if err != nil { if err != nil {
@ -131,7 +207,7 @@ func (g *Git) addLabel(repo Repository, api, label, color string) (labelID int64
return return
} }
resp, err = g.send(req) resp, err = g.send(req, repo.Owner.Login, repo.Name)
if err != nil { if err != nil {
err = fmt.Errorf("error creating label %s for %s: %s", label, u, err) err = fmt.Errorf("error creating label %s for %s: %s", label, u, err)
return return
@ -162,7 +238,7 @@ func (g *Git) setLabel(repo Repository, api, id, label, color string, remove []s
if err != nil { if err != nil {
return return
} }
resp, err := g.send(req) resp, err := g.send(req, repo.Owner.Login, repo.Name)
if err != nil { if err != nil {
return fmt.Errorf("error getting issue labels of %s: %s", u, err) return fmt.Errorf("error getting issue labels of %s: %s", u, err)
} }
@ -207,7 +283,7 @@ func (g *Git) setLabel(repo Repository, api, id, label, color string, remove []s
return return
} }
resp, err = g.send(req) resp, err = g.send(req, repo.Owner.Login, repo.Name)
if err != nil { if err != nil {
return fmt.Errorf("error setting issue label %s for %s: %s", label, u, err) return fmt.Errorf("error setting issue label %s for %s: %s", label, u, err)
} }

43
main.go
View file

@ -25,6 +25,8 @@ const (
commentAction = "X-Interceptor-Comment-Action" commentAction = "X-Interceptor-Comment-Action"
labelHeader = "X-Interceptor-Label" labelHeader = "X-Interceptor-Label"
removeLabelsHeader = "X-Interceptor-Remove-Labels" removeLabelsHeader = "X-Interceptor-Remove-Labels"
includeHeader = "X-Interceptor-Include-Repo"
excludeHeader = "X-Interceptor-Exclude-Repo"
githubEvent = "X-GitHub-Event" githubEvent = "X-GitHub-Event"
giteaEvent = "X-Gitea-Event" giteaEvent = "X-Gitea-Event"
@ -42,28 +44,33 @@ type Handler struct {
} }
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var valid bool
event := r.Header.Get(githubEvent) event := r.Header.Get(githubEvent)
switch event { switch event {
case "pull_request": case "pull_request":
h.prHandler(w, r) valid = h.prHandler(w, r)
case "push": case "push":
h.pushHandler(w, r) valid = h.pushHandler(w, r)
case "issue_comment": case "issue_comment":
h.commentHandler(w, r) valid = h.commentHandler(w, r)
default: default:
h.dumpHandler(w, r) h.dumpHandler(w, r)
} }
status := int(reflect.Indirect(reflect.ValueOf(w)).FieldByName("status").Int()) status := int(reflect.Indirect(reflect.ValueOf(w)).FieldByName("status").Int())
log.Printf("%s %s %s event:%s status:%d", r.RemoteAddr, r.Method, r.URL, event, status) log.Printf("%s %s %s event:%s valid:%t status:%d", r.RemoteAddr, r.Method, r.URL, event, valid, status)
} }
func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) { func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) (valid bool) {
var prb PullRequestBody var prb PullRequestBody
body, err := readBody(r.Body, &prb) body, err := readBody(r.Body, &prb)
if err != nil { if err != nil {
logError(w, err.Error()) logError(w, err.Error())
return return
} }
if valid = h.git.validate(r, body, prb.Repository); !valid {
w.WriteHeader(http.StatusBadRequest)
return
}
if contains(r.Header[prAction], prb.Action) { if contains(r.Header[prAction], prb.Action) {
if l := r.Header.Get(labelHeader); l != "" { if l := r.Header.Get(labelHeader); l != "" {
@ -85,33 +92,54 @@ func (h *Handler) prHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return
} }
func (h *Handler) pushHandler(w http.ResponseWriter, r *http.Request) { func (h *Handler) pushHandler(w http.ResponseWriter, r *http.Request) (valid bool) {
var pb PushBody var pb PushBody
body, err := readBody(r.Body, &pb) body, err := readBody(r.Body, &pb)
if err != nil { if err != nil {
logError(w, err.Error()) logError(w, err.Error())
return return
} }
if valid = h.git.validate(r, body, pb.Repository); !valid {
w.WriteHeader(http.StatusBadRequest)
return
}
push := r.Header.Get(pushRef) push := r.Header.Get(pushRef)
//if contains(r.Header[pushAction], pb.Ref) { //if contains(r.Header[pushAction], pb.Ref) {
if push != "" && push == pb.Ref { if push != "" && push == pb.Ref {
inc := r.Header[includeHeader]
exc := r.Header[excludeHeader]
if inc != nil && !contains(inc, pb.Repository.FullName) {
w.WriteHeader(http.StatusBadRequest)
return
}
if exc != nil && contains(exc, pb.Repository.FullName) {
w.WriteHeader(http.StatusBadRequest)
return
}
h := w.Header() h := w.Header()
h.Set(commitHeader, pb.After) h.Set(commitHeader, pb.After)
w.Write(body) w.Write(body)
return return
} }
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return
} }
func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) { func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) (valid bool) {
var cb CommentBody var cb CommentBody
body, err := readBody(r.Body, &cb) body, err := readBody(r.Body, &cb)
if err != nil { if err != nil {
logError(w, err.Error()) logError(w, err.Error())
return return
} }
if valid = h.git.validate(r, body, cb.Repository); !valid {
w.WriteHeader(http.StatusBadRequest)
return
}
if !contains(r.Header[commentAction], cb.Action) { if !contains(r.Header[commentAction], cb.Action) {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
@ -163,6 +191,7 @@ func (h *Handler) commentHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return
} }
func (h *Handler) dumpHandler(w http.ResponseWriter, r *http.Request) { func (h *Handler) dumpHandler(w http.ResponseWriter, r *http.Request) {

View file

@ -6,6 +6,7 @@ type PullRequestBody struct {
Action string `json:"action"` Action string `json:"action"`
Number int64 `json:"number"` Number int64 `json:"number"`
PullRequest PullRequest `json:"pull_request"` PullRequest PullRequest `json:"pull_request"`
Repository Repository `json:"repository"`
} }
type PullRequest struct { type PullRequest struct {
@ -34,6 +35,7 @@ type PostLabelsInt struct {
type PushBody struct { type PushBody struct {
Ref string `json:"ref"` Ref string `json:"ref"`
After string `json:"after"` After string `json:"after"`
Repository Repository `json:"repository"`
} }
type CommentBody struct { type CommentBody struct {
@ -54,6 +56,7 @@ type Comment struct {
type Repository struct { type Repository struct {
Name string `json:"name"` Name string `json:"name"`
FullName string `json:"full_name"`
Owner Owner `json:"owner"` Owner Owner `json:"owner"`
CloneURL string `json:"clone_url"` CloneURL string `json:"clone_url"`
} }