Skip to content

Commit aeb2237

Browse files
committed
Add vault sidecar to CR deployment
1 parent 870018c commit aeb2237

23 files changed

+353
-75
lines changed

.dockerignore

+7
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@
22
**.md
33
Dockerfile
44
docker-compose.yml
5+
.github
6+
.terraform
7+
.terraform.lock.hcl
8+
.git
9+
modules
10+
**.tf
11+
*.json

.github/workflows/deploy.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ jobs:
99
steps:
1010
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
1111

12-
- uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5
12+
- uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5
1313
with:
1414
go-version: '>=1.23.4'
1515

1616
- name: golangci-lint
17-
uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # v6
17+
uses: golangci/golangci-lint-action@051d91933864810ecd5e2ea2cfd98f6a5bca5347 # v6
1818
with:
1919
version: latest
2020

@@ -123,6 +123,9 @@ jobs:
123123
runs-on: ubuntu-24.04
124124
env:
125125
TF_VAR_project: ${{ secrets.GCLOUD_PROJECT }}
126+
TF_VAR_vault_addr: ${{ secrets.VAULT_ADDR }}
127+
TF_VAR_gh_app_id: ${{ secrets.GH_APP_ID }}
128+
TF_VAR_gh_install_id: ${{ secrets.GH_INSTALL_ID }}
126129
steps:
127130
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
128131

.github/workflows/validate-renovate.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
1616

17-
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
17+
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
1818
with:
1919
node-version: 20
2020

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
ghat
2+
.terraform
3+
.terraform.lock.hcl

Dockerfile

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
FROM golang:1.23-alpine3.20@sha256:6a8532e5441593becc88664617107ed567cb6862cb8b2d87eb33b7ee750f653c
1+
FROM golang:1.24-alpine3.20@sha256:9fed4022a220fb64327baa90cddfd98607f3b816cb4f5769187500571f73072d
22

33
WORKDIR /app
44

55
RUN adduser -S -G nobody ghat
66

77
COPY . ./
88

9-
RUN chown -R ghat:nobody /app
9+
RUN mkdir -p /vault/secrets && \
10+
chown -R ghat:nobody /app /vault
1011

11-
RUN go mod download && \
12+
RUN apk add --no-cache openssl bash && \
13+
go mod download && \
1214
go build -o /app/ghat && \
1315
go clean -cache -modcache
1416

1517
USER ghat
1618

17-
ENTRYPOINT ["/app/ghat"]
19+
ENTRYPOINT ["/app/docker-entrypoint.sh"]

LICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <https://unlicense.org>

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# ghat
2+
[![integration-test](https://github.com/libops/ghat/actions/workflows/deploy.yml/badge.svg)](https://github.com/libops/ghat/actions/workflows/deploy.yml)
3+
[![Go Report Card](https://goreportcard.com/badge/github.com/libops/ghat)](https://goreportcard.com/report/github.com/libops/ghat)
24

35
GitHub App Token
46

57
http service to allow receiving a scoped token from a GHA workflow.
8+
9+
## Requirements
10+
11+
The vault server that is configured in the sidecar needs the GitHub app private key
12+
13+
```
14+
vault kv put \
15+
-mount="secret" \
16+
"libops-ghat/github-app-key" \
17+
key="$(cat /path/to/private-key.pem | base64)"
18+
```

docker-entrypoint.sh

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
3+
set -eou pipefail
4+
5+
echo "Starting GitHub App Token service"
6+
7+
while [ ! -s "$GITHUB_APP_PRIVATE_KEY" ]; do
8+
echo "Waiting for $GITHUB_APP_PRIVATE_KEY"
9+
sleep 5
10+
done
11+
12+
if !openssl pkey -in "$GITHUB_APP_PRIVATE_KEY" -check -noout 2>/dev/null; then
13+
echo "ERROR: $GITHUB_APP_PRIVATE_KEY is not a valid private key (or unreadable)."
14+
exit 1
15+
fi
16+
17+
echo "GITHUB_APP_PRIVATE_KEY is ready"
18+
19+
exec /app/ghat

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.4
55
require (
66
github.com/bradleyfalzon/ghinstallation/v2 v2.13.0
77
github.com/google/go-github/v68 v68.0.0
8+
github.com/google/go-github/v69 v69.2.0
89
github.com/gorilla/mux v1.8.1
910
github.com/lestrrat-go/jwx/v2 v2.1.3
1011
)

go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1414
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1515
github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s=
1616
github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68=
17+
github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM=
1718
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
1819
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
1920
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=

lib/handler/handler.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func NewHandler() (*Handler, error) {
3535
installationId := loadEnvInt64("GITHUB_INSTALL_ID")
3636
privateKeyPath := loadEnv("GITHUB_APP_PRIVATE_KEY")
3737
tr := http.DefaultTransport
38-
itr, err := ghinstallation.NewKeyFromFile(tr, appId, installationId, privateKeyPath)
38+
itr, err := ghinstallation.NewAppsTransportKeyFromFile(tr, appId, privateKeyPath)
3939
if err != nil {
4040
return nil, fmt.Errorf("failed to create installation transport: %v", err)
4141
}

lib/handler/middleware.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
type contextKey string
1717

18-
const claimsKey contextKey = "claims"
18+
const claimsKey contextKey = "githubClaims"
1919

2020
const githubKeysURL = "https://token.actions.githubusercontent.com/.well-known/jwks"
2121

@@ -73,10 +73,12 @@ func JWTAuthMiddleware(next http.Handler) http.Handler {
7373
})
7474
}
7575

76-
func verifyJWT(tokenString string) (*GitHubClaims, error) {
76+
func verifyJWT(tokenString string) (GitHubClaims, error) {
77+
var claims GitHubClaims
78+
7779
keySet, err := fetchJWKS()
7880
if err != nil {
79-
return nil, fmt.Errorf("Unable to fetch JWKS: %v", err)
81+
return claims, fmt.Errorf("Unable to fetch JWKS: %v", err)
8082
}
8183

8284
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -88,23 +90,22 @@ func verifyJWT(tokenString string) (*GitHubClaims, error) {
8890
jwt.WithContext(ctx),
8991
jwt.WithVerify(true))
9092
if err != nil {
91-
return nil, fmt.Errorf("Unable to parse token: %v", err)
93+
return claims, fmt.Errorf("Unable to parse token: %v", err)
9294
}
9395

9496
if err := validateClaims(token); err != nil {
95-
return nil, fmt.Errorf("Unable to validate claims: %v", err)
97+
return claims, fmt.Errorf("Unable to validate claims: %v", err)
9698
}
9799
rawClaims, err := json.Marshal(token)
98100
if err != nil {
99-
return nil, fmt.Errorf("failed to marshal claims: %v", err)
101+
return claims, fmt.Errorf("failed to marshal claims: %v", err)
100102
}
101103

102-
var claims GitHubClaims
103104
if err := json.Unmarshal(rawClaims, &claims); err != nil {
104-
return nil, fmt.Errorf("failed to unmarshal claims: %v", err)
105+
return claims, fmt.Errorf("failed to unmarshal claims: %v", err)
105106
}
106107

107-
return &claims, nil
108+
return claims, nil
108109
}
109110

110111
func fetchJWKS() (jwk.Set, error) {

lib/handler/registration_token.go renamed to lib/handler/repo_admin.go

+5-7
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,30 @@ import (
44
"encoding/json"
55
"log/slog"
66
"net/http"
7+
"strings"
78

89
"github.com/google/go-github/v68/github"
910
)
1011

1112
func (h *Handler) RepoAdminToken(w http.ResponseWriter, r *http.Request) {
12-
claims, ok := r.Context().Value(claimsKey).(GitHubClaims)
13-
if !ok {
14-
http.Error(w, "Unauthorized: missing claims in context", http.StatusUnauthorized)
15-
return
16-
}
13+
claims := r.Context().Value(claimsKey).(GitHubClaims)
1714
if claims.Repository == "" {
1815
http.Error(w, "Invalid request: repository claim is empty", http.StatusBadRequest)
1916
return
2017
}
2118

2219
opts := &github.InstallationTokenOptions{
2320
Repositories: []string{
24-
claims.Repository,
21+
strings.Split(claims.Repository, "/")[1],
2522
},
2623
Permissions: &github.InstallationPermissions{
2724
Administration: github.Ptr("write"),
25+
Secrets: github.Ptr("write"),
2826
},
2927
}
3028
token, _, err := h.githubClient.Apps.CreateInstallationToken(r.Context(), h.githubInstallationId, opts)
3129
if err != nil {
32-
slog.Error("Error fetching scoped token", "err", err, "claims", claims)
30+
slog.Error("Error fetching scoped token", "err", err, "claims", claims, "opts", opts)
3331
http.Error(w, "Internal error.", http.StatusInternalServerError)
3432
return
3533
}

main.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@ func main() {
1717
}
1818

1919
r := mux.NewRouter()
20-
r.Use(handler.LoggingMiddleware)
21-
r.Use(handler.JWTAuthMiddleware)
22-
r.HandleFunc("/repo/admin", wh.RepoAdminToken).Methods("POST")
23-
r.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
24-
_, err := w.Write([]byte(`ok`))
20+
r.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
21+
_, err := w.Write([]byte("ok"))
2522
if err != nil {
2623
w.WriteHeader(http.StatusInternalServerError)
27-
slog.Error("Unable to write for healthcheck")
24+
slog.Error("Unable to write for healthcheck", "error", err)
2825
}
2926
}).Methods("GET")
3027

28+
authRouter := r.PathPrefix("/").Subrouter()
29+
authRouter.Use(handler.LoggingMiddleware)
30+
authRouter.Use(handler.JWTAuthMiddleware)
31+
authRouter.HandleFunc("/repo/admin", wh.RepoAdminToken).Methods("GET")
32+
authRouter.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
33+
_, err := w.Write([]byte(`ok`))
34+
if err != nil {
35+
w.WriteHeader(http.StatusInternalServerError)
36+
slog.Error("Unable to write for healthcheck", "error", err)
37+
}
38+
})
3139
port := os.Getenv("PORT")
3240
if port == "" {
3341
port = "8080"

0 commit comments

Comments
 (0)