Skip to content

Commit

Permalink
new askpass sidecar (#896) (#905)
Browse files Browse the repository at this point in the history
* initial implemetation of the askpass logic

* wip: makefile updates

* found the missing bit

* local askpass img  in the reconciler mgr

* linting issues

* cloud metadata library addition

* pr feedback

Co-authored-by: Mike Borozdin <[email protected]>
  • Loading branch information
sdowell and mikebz authored Sep 28, 2023
1 parent 784ca15 commit 10fd7f7
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 3 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ HYDRATION_CONTROLLER_WITH_SHELL_IMAGE := $(HYDRATION_CONTROLLER_IMAGE)-with-shel
OCI_SYNC_IMAGE := oci-sync
HELM_SYNC_IMAGE := helm-sync
NOMOS_IMAGE := nomos
ASKPASS_IMAGE := gcenode-askpass-sidecar

# nomos binary for local run.
NOMOS_LOCAL := $(BIN_DIR)/linux_amd64/nomos
Expand Down Expand Up @@ -146,6 +147,7 @@ HYDRATION_CONTROLLER_WITH_SHELL_GCR := $(REGISTRY)/$(HYDRATION_CONTROLLER_WITH_S
OCI_SYNC_GCR := $(REGISTRY)/$(OCI_SYNC_IMAGE)
HELM_SYNC_GCR := $(REGISTRY)/$(HELM_SYNC_IMAGE)
NOMOS_GCR := $(REGISTRY)/$(NOMOS_IMAGE)
ASKPASS_GCR := $(REGISTRY)/$(ASKPASS_IMAGE)
# Full image tags as given on gcr.io
RECONCILER_TAG := $(RECONCILER_GCR):$(IMAGE_TAG)
RECONCILER_MANAGER_TAG := $(RECONCILER_MANAGER_GCR):$(IMAGE_TAG)
Expand All @@ -155,6 +157,7 @@ HYDRATION_CONTROLLER_WITH_SHELL_TAG := $(HYDRATION_CONTROLLER_WITH_SHELL_GCR):$(
OCI_SYNC_TAG := $(OCI_SYNC_GCR):$(IMAGE_TAG)
HELM_SYNC_TAG := $(HELM_SYNC_GCR):$(IMAGE_TAG)
NOMOS_TAG := $(NOMOS_GCR):$(IMAGE_TAG)
ASKPASS_TAG := $(ASKPASS_GCR):$(IMAGE_TAG)

DOCKER_RUN_ARGS = \
$(DOCKER_INTERACTIVE) \
Expand Down
14 changes: 14 additions & 0 deletions Makefile.build
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ build-images: install-helm install-kustomize
-f build/all/Dockerfile \
--build-arg VERSION=${VERSION} \
.
@echo "+++ Building the Askpass image: $(ASKPASS_TAG)"
@docker buildx build $(DOCKER_BUILD_QUIET) \
--target $(ASKPASS_IMAGE) \
-t $(ASKPASS_TAG) \
-f build/all/Dockerfile \
--build-arg VERSION=${VERSION} \
.
@echo "+++ Building the Nomos image: $(NOMOS_TAG)"
@docker buildx build $(DOCKER_BUILD_QUIET) \
--target $(NOMOS_IMAGE) \
Expand Down Expand Up @@ -133,6 +140,7 @@ push-images:
docker push $(OCI_SYNC_TAG)
docker push $(HELM_SYNC_TAG)
docker push $(NOMOS_TAG)
docker push $(ASKPASS_TAG)

# Deprecated alias of push-images. Remove this once unused.
.PHONY: push-images-multirepo
Expand All @@ -150,6 +158,7 @@ pull-images:
docker pull $(OCI_SYNC_TAG)
docker pull $(HELM_SYNC_TAG)
docker pull $(NOMOS_TAG)
docker pull $(ASKPASS_TAG)

# Deprecated alias of pull-images. Remove this once unused.
.PHONY: pull-images-multirepo
Expand All @@ -167,6 +176,7 @@ retag-images:
docker tag $(OLD_REGISTRY)/$(OCI_SYNC_IMAGE):$(OLD_IMAGE_TAG) $(OCI_SYNC_TAG)
docker tag $(OLD_REGISTRY)/$(HELM_SYNC_IMAGE):$(OLD_IMAGE_TAG) $(HELM_SYNC_TAG)
docker tag $(OLD_REGISTRY)/$(NOMOS_IMAGE):$(OLD_IMAGE_TAG) $(NOMOS_TAG)
docker tag $(OLD_REGISTRY)/$(ASKPASS_IMAGE):$(OLD_IMAGE_TAG) $(ASKPASS_TAG)

# Deprecated alias of retag-images. Remove this once unused.
.PHONY: retag-images-multirepo
Expand All @@ -192,6 +202,7 @@ build-manifests-oss: "$(GOBIN)/addlicense" "$(BIN_DIR)/kustomize" $(OUTPUT_DIR)
@ echo " $(ADMISSION_WEBHOOK_IMAGE): $(ADMISSION_WEBHOOK_TAG)"
@ echo " $(OCI_SYNC_IMAGE): $(OCI_SYNC_TAG)"
@ echo " $(HELM_SYNC_IMAGE): $(HELM_SYNC_TAG)"
@ echo " $(ASKPASS_IMAGE): $(ASKPASS_TAG)"
@ rm -f $(OSS_MANIFEST_STAGING_DIR)/*
@ "$(BIN_DIR)/kustomize" build --load-restrictor=LoadRestrictionsNone manifests/oss \
| sed \
Expand All @@ -200,6 +211,7 @@ build-manifests-oss: "$(GOBIN)/addlicense" "$(BIN_DIR)/kustomize" $(OUTPUT_DIR)
-e "s|HELM_SYNC_IMAGE_NAME|$(HELM_SYNC_TAG)|g" \
-e "s|HYDRATION_CONTROLLER_IMAGE_NAME|$(HYDRATION_CONTROLLER_TAG)|g" \
-e "s|RECONCILER_MANAGER_IMAGE_NAME|$(RECONCILER_MANAGER_TAG)|g" \
-e "s|ASKPASS_IMAGE_NAME|$(ASKPASS_TAG)|g" \
> $(OSS_MANIFEST_STAGING_DIR)/config-sync-manifest.yaml
@ "$(GOBIN)/addlicense" $(OSS_MANIFEST_STAGING_DIR)/config-sync-manifest.yaml

Expand All @@ -224,6 +236,7 @@ build-manifests-operator: "$(GOBIN)/addlicense" "$(BIN_DIR)/kustomize" $(OUTPUT_
@ echo " $(ADMISSION_WEBHOOK_IMAGE): $(ADMISSION_WEBHOOK_TAG)"
@ echo " $(OCI_SYNC_IMAGE): $(OCI_SYNC_TAG)"
@ echo " $(HELM_SYNC_IMAGE): $(HELM_SYNC_TAG)"
@ echo " $(ASKPASS_IMAGE): $(ASKPASS_TAG)"
@ rm -f $(NOMOS_MANIFEST_STAGING_DIR)/*
@ "$(BIN_DIR)/kustomize" build --load-restrictor=LoadRestrictionsNone manifests/operator \
| sed \
Expand All @@ -233,6 +246,7 @@ build-manifests-operator: "$(GOBIN)/addlicense" "$(BIN_DIR)/kustomize" $(OUTPUT_
-e "s|HYDRATION_CONTROLLER_IMAGE_NAME|$(HYDRATION_CONTROLLER_TAG)|g" \
-e "s|RECONCILER_MANAGER_IMAGE_NAME|$(RECONCILER_MANAGER_TAG)|g" \
-e "s|WEBHOOK_IMAGE_NAME|$(ADMISSION_WEBHOOK_TAG)|g" \
-e "s|ASKPASS_IMAGE_NAME|$(ASKPASS_TAG)|g" \
> $(NOMOS_MANIFEST_STAGING_DIR)/config-sync-manifest.yaml
@ "$(GOBIN)/addlicense" $(NOMOS_MANIFEST_STAGING_DIR)/config-sync-manifest.yaml

Expand Down
12 changes: 11 additions & 1 deletion build/all/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on \
./cmd/hydration-controller \
./cmd/admission-webhook \
./cmd/oci-sync \
./cmd/helm-sync
./cmd/helm-sync \
./cmd/gcenode-askpass-sidecar

# Concatenate vendored licenses into LICENSES.txt
# Built in the container to include binary licenses (helm & kustomize)
Expand Down Expand Up @@ -129,6 +130,15 @@ COPY --from=bins /workspace/LICENSES.txt LICENSES.txt
USER nonroot:nonroot
ENTRYPOINT ["/admission-webhook"]

# Askpass image
FROM gcr.io/distroless/static:nonroot as gcenode-askpass-sidecar
WORKDIR /
COPY --from=bins /go/bin/gcenode-askpass-sidecar gcenode-askpass-sidecar
COPY --from=bins /workspace/LICENSE LICENSE
COPY --from=bins /workspace/LICENSES.txt LICENSES.txt
USER nonroot:nonroot
ENTRYPOINT ["/gcenode-askpass-sidecar"]

# Nomos image
# Not used by Config Sync backend components. Intended for use cases with the
# nomos CLI (e.g. containerized CI/CD)
Expand Down
102 changes: 102 additions & 0 deletions cmd/gcenode-askpass-sidecar/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2023 Google LLC
//
// 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 main

import (
"flag"
"fmt"
"net/http"

"cloud.google.com/go/compute/metadata"
"k8s.io/klog/v2/klogr"
"kpt.dev/configsync/pkg/askpass"
"kpt.dev/configsync/pkg/util"
utillog "kpt.dev/configsync/pkg/util/log"
)

// all the flags and their usage.
var flHelp = flag.Bool("help",
false,
"print help and usage information")

var flPort = flag.Int("port",
util.EnvInt("ASKPASS_PORT", 9102),
"port to listen on")

var flGsaEmail = flag.String("email",
util.EnvString("GSA_EMAIL", ""),
"Google Service Account for authentication")

var flErrorFile = flag.String("error-file",
util.EnvString("ASKPASS_ERROR_FILE", ""),
"the name of a file into which errors will be written defaults to \"\", disabling error reporting")

var flRoot = flag.String("root",
util.EnvString("ASKPASS_ROOT", util.EnvString("HOME", "")+"/askpass"),
"the root directory for askpass")

// main function is designed only to deal with the environment. i.e. parse
// user input and make sure we can set up a server. All the logic outside
// of OS and network interractions should be in the package with the logic
// for askpass
func main() {
// if people are looking for help we are not going to launch anything
// assuming that they didn't mean to start the askpass process.
if *flHelp {
flag.Usage()
return
}

utillog.Setup()
log := utillog.NewLogger(klogr.New(), *flRoot, *flErrorFile)

log.Info("starting askpass with arguments", "--port", *flPort,
"--email", *flGsaEmail, "--error-file", *flErrorFile, "--root", *flRoot)

if *flPort == 0 {
utillog.HandleError(log, true,
"ERROR: port can not be zero")
}

if *flRoot == "" {
utillog.HandleError(log, true, "root cannot be empty")
}

var gsaEmail string
var err error
// for getting the GSA email we have several scenarios
// the first one is that the user provides it to us.
// the second scenario is that it's not provided but we can get the
// Compute Engine default service account from the metadata server.
if *flGsaEmail != "" {
gsaEmail = *flGsaEmail
} else if metadata.OnGCE() {
gsaEmail, err = metadata.Email("")
if err != nil {
utillog.HandleError(log, false, "error in http.ListenAndServe: %v", err)
}
} else {
utillog.HandleError(log, true,
"ERROR: GSA email can not be empty")
}

aps := &askpass.Server{
Email: gsaEmail,
}
http.HandleFunc("/git_askpass", aps.GitAskPassHandler)

if err := http.ListenAndServe(fmt.Sprintf(":%d", *flPort), nil); err != nil {
utillog.HandleError(log, false, "error in http.ListenAndServe: %v", err)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module kpt.dev/configsync
go 1.20

require (
cloud.google.com/go/compute/metadata v0.2.3
cloud.google.com/go/monitoring v1.8.0
cloud.google.com/go/trace v1.4.0
contrib.go.opencensus.io/exporter/ocagent v0.7.0
Expand Down Expand Up @@ -62,7 +63,6 @@ require (

require (
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
Expand Down
2 changes: 1 addition & 1 deletion manifests/templates/reconciler-manager-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ data:
cpu: "10m"
memory: "200Mi"
- name: gcenode-askpass-sidecar
image: gcr.io/config-management-release/gcenode-askpass-sidecar:v1.0.5
image: ASKPASS_IMAGE_NAME
args: ["--port=9102", "--logtostderr"]
imagePullPolicy: IfNotPresent
terminationMessagePolicy: File
Expand Down
93 changes: 93 additions & 0 deletions pkg/askpass/askpass.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 Google LLC
//
// 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 askpass is designed to be used in the askpass sidecar
// to provide GSA authentication services.
package askpass

import (
"context"
"fmt"
"net/http"
"time"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"k8s.io/klog/v2"
)

// Server contains server wide state and settings for the askpass sidecar
type Server struct {
Email string
token *oauth2.Token
}

// GitAskPassHandler is the main method for clients to ask us for
// credentials
func (aps *Server) GitAskPassHandler(w http.ResponseWriter, r *http.Request) {
klog.Infof("handling new askpass request from host: %s", r.Host)

if aps.needNewToken() {
err := aps.retrieveNewToken(r.Context())
if err != nil {
klog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
klog.Infof("reusing existing oauth2 token, type: %s, expiration: %v",
aps.token.TokenType, aps.token.Expiry)
}

// this this point we should be equipped with all the credentials
// and it's just a matter of sending it back to the caller.
password := aps.token.AccessToken
w.WriteHeader(http.StatusOK)
if _, err := fmt.Fprintf(w, "username=%s\npassword=%s", aps.Email, password); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
klog.Error(err)
}
}

// needNewToken will tell us if we have an Oauth2 token that is
// has not expired yet.
func (aps *Server) needNewToken() bool {
if aps.token == nil {
return true
}

if time.Now().After(aps.token.Expiry) {
return true
}

return false
}

// retrieveNewToken will use the default credentials in order to
// fetch to fetch a new token. Note the side effect that the
// server token will be replaced in case of a successful retrieval.
func (aps *Server) retrieveNewToken(ctx context.Context) error {
creds, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/cloud-platform")
if err != nil {
return fmt.Errorf("error calling google.FindDefaultCredentials: %w", err)
}
aps.token, err = creds.TokenSource.Token()
if err != nil {
return fmt.Errorf("error retrieveing TokenSource.Token: %w", err)
}

klog.Infof("retrieved new Oauth2 token, type: %s, expiration: %v",
aps.token.TokenType, aps.token.Expiry)
return nil
}
Loading

0 comments on commit 10fd7f7

Please sign in to comment.