diff --git a/apiserver/controllers/instances.go b/apiserver/controllers/instances.go index e4011b3e..c9ef6533 100644 --- a/apiserver/controllers/instances.go +++ b/apiserver/controllers/instances.go @@ -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) - } -} diff --git a/apiserver/controllers/metadata.go b/apiserver/controllers/metadata.go new file mode 100644 index 00000000..c88e68cb --- /dev/null +++ b/apiserver/controllers/metadata.go @@ -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) + } +} diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index 161f9cee..244992fe 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -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") diff --git a/auth/context.go b/auth/context.go index 27845288..64273530 100644 --- a/auth/context.go +++ b/auth/context.go @@ -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 } diff --git a/params/params.go b/params/params.go index 5eaa6d71..2a0547ff 100644 --- a/params/params.go +++ b/params/params.go @@ -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"` +} diff --git a/runner/common/pool.go b/runner/common/pool.go index 9024a930..b44488ee 100644 --- a/runner/common/pool.go +++ b/runner/common/pool.go @@ -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 diff --git a/runner/metadata.go b/runner/metadata.go new file mode 100644 index 00000000..9d0a2769 --- /dev/null +++ b/runner/metadata.go @@ -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 +} diff --git a/runner/pool/pool.go b/runner/pool/pool.go index 39a96b42..8b08b103 100644 --- a/runner/pool/pool.go +++ b/runner/pool/pool.go @@ -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() +}