Add root CA bundle metadata URL

Thic change adds a metadata endpoint that returns a list of root CA
certificates a runner must install in order to be able to validate all
relevant API endpoints it may require. This includes any GHES API that
runs on a self signed certificate.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2023-08-28 09:44:18 +00:00
parent f463a41ce2
commit a26907fb91
8 changed files with 159 additions and 17 deletions

View file

@ -316,19 +316,3 @@ func (a *APIController) InstanceStatusMessageHandler(w http.ResponseWriter, r *h
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (a *APIController) InstanceGithubRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token, err := a.r.GetInstanceGithubRegistrationToken(ctx)
if err != nil {
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(token)); err != nil {
log.Printf("failed to encode response: %q", err)
}
}

View file

@ -0,0 +1,52 @@
// Copyright 2023 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package controllers
import (
"encoding/json"
"log"
"net/http"
)
func (a *APIController) InstanceGithubRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token, err := a.r.GetInstanceGithubRegistrationToken(ctx)
if err != nil {
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(token)); err != nil {
log.Printf("failed to encode response: %q", err)
}
}
func (a *APIController) RootCertificateBundleHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
bundle, err := a.r.GetRootCertificateBundle(ctx)
if err != nil {
handleError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(bundle); err != nil {
log.Printf("failed to encode response: %q", err)
}
}

View file

@ -108,10 +108,18 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl
callbackRouter.Handle("/status", http.HandlerFunc(han.InstanceStatusMessageHandler)).Methods("POST", "OPTIONS")
callbackRouter.Use(instanceMiddleware.Middleware)
///////////////////
// Metadata URLs //
///////////////////
metadataRouter := apiSubRouter.PathPrefix("/metadata").Subrouter()
metadataRouter.Use(instanceMiddleware.Middleware)
// Registration token
metadataRouter.Handle("/runner-registration-token/", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS")
metadataRouter.Handle("/runner-registration-token", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS")
metadataRouter.Use(instanceMiddleware.Middleware)
metadataRouter.Handle("/system/cert-bundle/", http.HandlerFunc(han.RootCertificateBundleHandler)).Methods("GET", "OPTIONS")
metadataRouter.Handle("/system/cert-bundle", http.HandlerFunc(han.RootCertificateBundleHandler)).Methods("GET", "OPTIONS")
// Login
authRouter := apiSubRouter.PathPrefix("/auth").Subrouter()
authRouter.Handle("/{login:login\\/?}", http.HandlerFunc(han.LoginHandler)).Methods("POST", "OPTIONS")

View file

@ -17,6 +17,7 @@ package auth
import (
"context"
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm/params"
)
@ -38,6 +39,7 @@ const (
instanceEntityKey contextFlags = "entity"
instanceRunnerStatus contextFlags = "status"
instanceTokenFetched contextFlags = "tokenFetched"
instanceParams contextFlags = "instanceParams"
)
func SetInstanceID(ctx context.Context, id string) context.Context {
@ -64,6 +66,23 @@ func InstanceTokenFetched(ctx context.Context) bool {
return elem.(bool)
}
func SetInstanceParams(ctx context.Context, instance params.Instance) context.Context {
return context.WithValue(ctx, instanceParams, instance)
}
func InstanceParams(ctx context.Context) (params.Instance, error) {
elem := ctx.Value(instanceParams)
if elem == nil {
return params.Instance{}, runnerErrors.ErrNotFound
}
instanceParams, ok := elem.(params.Instance)
if !ok {
return params.Instance{}, runnerErrors.ErrNotFound
}
return instanceParams, nil
}
func SetInstanceRunnerStatus(ctx context.Context, val params.RunnerStatus) context.Context {
return context.WithValue(ctx, instanceRunnerStatus, val)
}
@ -130,6 +149,7 @@ func PopulateInstanceContext(ctx context.Context, instance params.Instance) cont
ctx = SetInstancePoolID(ctx, instance.PoolID)
ctx = SetInstanceRunnerStatus(ctx, instance.RunnerStatus)
ctx = SetInstanceTokenFetched(ctx, instance.TokenFetched)
ctx = SetInstanceParams(ctx, instance)
return ctx
}

View file

@ -15,7 +15,11 @@
package params
import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
@ -410,6 +414,36 @@ type GithubCredentials struct {
CABundle []byte `json:"ca_bundle,omitempty"`
}
func (g GithubCredentials) RootCertificateBundle() (CertificateBundle, error) {
if len(g.CABundle) == 0 {
return CertificateBundle{}, nil
}
ret := map[string][]byte{}
var block *pem.Block
var rest []byte = g.CABundle
for {
block, rest = pem.Decode(rest)
if block == nil {
break
}
pub, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return CertificateBundle{}, err
}
out := &bytes.Buffer{}
if err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: block.Bytes}); err != nil {
return CertificateBundle{}, err
}
ret[fmt.Sprintf("%d", pub.SerialNumber)] = out.Bytes()
}
return CertificateBundle{
RootCertificates: ret,
}, nil
}
// used by swagger client generated code
type Credentials []GithubCredentials
@ -513,3 +547,7 @@ type HookInfo struct {
Active bool `json:"active"`
InsecureSSL bool `json:"insecure_ssl"`
}
type CertificateBundle struct {
RootCertificates map[string][]byte `json:"root_certificates"`
}

View file

@ -49,6 +49,8 @@ type PoolManager interface {
GetWebhookInfo(ctx context.Context) (params.HookInfo, error)
UninstallWebhook(ctx context.Context) error
RootCABundle() (params.CertificateBundle, error)
// PoolManager lifecycle functions. Start/stop pool.
Start() error
Stop() error

34
runner/metadata.go Normal file
View file

@ -0,0 +1,34 @@
package runner
import (
"context"
"log"
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm/auth"
"github.com/cloudbase/garm/params"
"github.com/pkg/errors"
)
func (r *Runner) GetRootCertificateBundle(ctx context.Context) (params.CertificateBundle, error) {
instance, err := auth.InstanceParams(ctx)
if err != nil {
log.Printf("failed to get instance params: %s", err)
return params.CertificateBundle{}, runnerErrors.ErrUnauthorized
}
poolMgr, err := r.getPoolManagerFromInstance(ctx, instance)
if err != nil {
return params.CertificateBundle{}, errors.Wrap(err, "fetching pool manager for instance")
}
bundle, err := poolMgr.RootCABundle()
if err != nil {
log.Printf("failed to get root CA bundle: %s", err)
// The root CA bundle is invalid. Return an empty bundle to the runner and log the event.
return params.CertificateBundle{
RootCertificates: make(map[string][]byte),
}, nil
}
return bundle, nil
}

View file

@ -1643,3 +1643,7 @@ func (r *basePoolManager) UninstallWebhook(ctx context.Context) error {
func (r *basePoolManager) GetWebhookInfo(ctx context.Context) (params.HookInfo, error) {
return r.helper.GetHookInfo(ctx)
}
func (r *basePoolManager) RootCABundle() (params.CertificateBundle, error) {
return r.credsDetails.RootCertificateBundle()
}