mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-01 11:26:29 +02:00
* Add new configurable bootstrap writers (#2405) This PR adds the ability to configure different backends to use for storing modifications to the zero bootstrap config. The two currently implemented backends allow writing changes to a file or to a Kubernetes secret. Backend selection is determined by the scheme in a URI passed to the flag '--config-writeback-uri'. In a Kubernetes environment, where the bootstrap config is mounted into the pod from a secret, this option allows Pomerium to write changes back to the secret, as writes to the mounted secret file on disk are not persisted. * Use env vars for bootstrap config filepath/writeback uri * linter pass and code cleanup * Add new config writer options mechanism This moves the encryption cipher parameter out of the WriteConfig() method in the ConfigWriter interface and into a new ConfigWriterOptions struct. Options (e.g. cipher) can be applied to an existing ConfigWriter to allow customizing implementation-specific behavior. * Code cleanup/lint fixes * Move vendored k8s code into separate package, and add license header and package comment
161 lines
4 KiB
Go
161 lines
4 KiB
Go
package k8s
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/zero/bootstrap/writers"
|
|
"github.com/pomerium/pomerium/internal/zero/bootstrap/writers/k8s/rest"
|
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster"
|
|
)
|
|
|
|
func init() {
|
|
writers.RegisterBuilder("secret", newInClusterSecretWriter)
|
|
}
|
|
|
|
type secretWriter struct {
|
|
opts writers.ConfigWriterOptions
|
|
client *http.Client
|
|
apiserverURL *url.URL
|
|
namespace string
|
|
name string
|
|
key string
|
|
}
|
|
|
|
// WithOptions implements writers.ConfigWriter.
|
|
func (w *secretWriter) WithOptions(opts writers.ConfigWriterOptions) writers.ConfigWriter {
|
|
clone := *w
|
|
clone.opts = opts
|
|
return &clone
|
|
}
|
|
|
|
func newSecretWriterForConfig(uri *url.URL, config *rest.Config) (writers.ConfigWriter, error) {
|
|
parts := strings.SplitN(path.Join(uri.Host, uri.Path), "/", 3)
|
|
if len(parts) != 3 || parts[1] == "" || parts[2] == "" {
|
|
return nil, fmt.Errorf("invalid secret uri %q, expecting format \"secret://namespace/name/key\"", uri.String())
|
|
} else if parts[0] == "" {
|
|
return nil, fmt.Errorf(`invalid secret uri %q (did you mean "secret:/%s"?)`, uri.String(), uri.Path)
|
|
}
|
|
u, err := url.Parse(config.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
transport.TLSClientConfig = config.TLSClientConfig
|
|
|
|
client := &http.Client{
|
|
Transport: &roundTripper{
|
|
bearerToken: config.BearerToken,
|
|
base: transport,
|
|
},
|
|
}
|
|
|
|
return &secretWriter{
|
|
client: client,
|
|
apiserverURL: u,
|
|
namespace: parts[0],
|
|
name: parts[1],
|
|
key: parts[2],
|
|
}, nil
|
|
}
|
|
|
|
func newInClusterSecretWriter(uri *url.URL) (writers.ConfigWriter, error) {
|
|
config, err := rest.InClusterConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newSecretWriterForConfig(uri, config)
|
|
}
|
|
|
|
// WriteConfig implements ConfigWriter.
|
|
func (w *secretWriter) WriteConfig(ctx context.Context, src *cluster_api.BootstrapConfig) error {
|
|
u := w.apiserverURL.ResolveReference(&url.URL{
|
|
Path: path.Join("/api/v1/namespaces", w.namespace, "secrets", w.name),
|
|
RawQuery: url.Values{
|
|
"fieldManager": {"pomerium-zero"},
|
|
"force": {"true"},
|
|
}.Encode(),
|
|
})
|
|
data, err := json.Marshal(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if w.opts.Cipher != nil {
|
|
data = cryptutil.Encrypt(w.opts.Cipher, data, nil)
|
|
}
|
|
encodedData := base64.StdEncoding.EncodeToString(data)
|
|
|
|
patch, _ := yaml.Marshal(map[string]any{
|
|
"apiVersion": "v1",
|
|
"kind": "Secret",
|
|
"metadata": map[string]any{
|
|
"name": w.name,
|
|
"namespace": w.namespace,
|
|
},
|
|
"data": map[string]string{
|
|
w.key: encodedData,
|
|
},
|
|
})
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, u.String(), bytes.NewReader(patch))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/apply-patch+yaml")
|
|
|
|
resp, err := w.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
switch resp.StatusCode {
|
|
case http.StatusOK, http.StatusCreated:
|
|
return nil
|
|
case http.StatusForbidden:
|
|
if resp.Header.Get("Content-Type") == "application/json" {
|
|
// log the detailed status message if available
|
|
status, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
break
|
|
}
|
|
var buf bytes.Buffer
|
|
err = json.Compact(&buf, status)
|
|
if err != nil {
|
|
break
|
|
}
|
|
log.Ctx(ctx).Error().
|
|
RawJSON("response", buf.Bytes()).
|
|
Msgf("%s %s: %s", req.Method, req.URL, resp.Status)
|
|
}
|
|
}
|
|
return fmt.Errorf("unexpected status: %s", resp.Status)
|
|
}
|
|
|
|
var _ writers.ConfigWriter = (*secretWriter)(nil)
|
|
|
|
type roundTripper struct {
|
|
base http.RoundTripper
|
|
bearerToken string
|
|
}
|
|
|
|
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if req.Header.Get("Authorization") == "" {
|
|
req = req.Clone(req.Context())
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rt.bearerToken))
|
|
}
|
|
return rt.base.RoundTrip(req)
|
|
}
|