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:
parent
f463a41ce2
commit
a26907fb91
8 changed files with 159 additions and 17 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
52
apiserver/controllers/metadata.go
Normal file
52
apiserver/controllers/metadata.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
34
runner/metadata.go
Normal 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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue