diff --git a/internal/zero/bootstrap/bootstrap.go b/internal/zero/bootstrap/bootstrap.go index 13e0d0147..cfa70d08b 100644 --- a/internal/zero/bootstrap/bootstrap.go +++ b/internal/zero/bootstrap/bootstrap.go @@ -104,7 +104,7 @@ func (svc *Source) updateAndSave(ctx context.Context) error { return nil } - err = SaveBootstrapConfig(ctx, svc.writer, cfg, svc.fileCipher) + err = SaveBootstrapConfig(ctx, svc.writer, cfg) if err != nil { log.Ctx(ctx).Error().Err(err). Msg("failed to save bootstrap config to file, note it may prevent Pomerium from starting up in case of connectivity issues") diff --git a/internal/zero/bootstrap/file.go b/internal/zero/bootstrap/file.go index 21d116cbc..f401cff7a 100644 --- a/internal/zero/bootstrap/file.go +++ b/internal/zero/bootstrap/file.go @@ -42,8 +42,8 @@ func LoadBootstrapConfigFromFile(fp string, cipher cipher.AEAD) (*cluster_api.Bo } // SaveBootstrapConfig saves the bootstrap configuration to a file. -func SaveBootstrapConfig(ctx context.Context, writer writers.ConfigWriter, src *cluster_api.BootstrapConfig, cipher cipher.AEAD) error { - err := writer.WriteConfig(ctx, src, cipher) +func SaveBootstrapConfig(ctx context.Context, writer writers.ConfigWriter, src *cluster_api.BootstrapConfig) error { + err := writer.WriteConfig(ctx, src) if err != nil { health.ReportError(health.ZeroBootstrapConfigSave, err) } else { diff --git a/internal/zero/bootstrap/new.go b/internal/zero/bootstrap/new.go index 36cde4566..c80c6530f 100644 --- a/internal/zero/bootstrap/new.go +++ b/internal/zero/bootstrap/new.go @@ -55,6 +55,12 @@ func New(secret []byte, fileCachePath *string, writer writers.ConfigWriter, api return nil, fmt.Errorf("init cypher: %w", err) } + if writer != nil { + writer = writer.WithOptions(writers.ConfigWriterOptions{ + Cipher: cipher, + }) + } + svc := &Source{ api: api, source: source{ready: make(chan struct{})}, diff --git a/internal/zero/bootstrap/writers/filesystem/file.go b/internal/zero/bootstrap/writers/filesystem/file.go index 987e785b6..40c09d388 100644 --- a/internal/zero/bootstrap/writers/filesystem/file.go +++ b/internal/zero/bootstrap/writers/filesystem/file.go @@ -2,7 +2,6 @@ package filesystem import ( "context" - "crypto/cipher" "encoding/json" "fmt" "net/url" @@ -14,30 +13,42 @@ import ( ) func init() { - writers.RegisterBuilder("file", func(uri *url.URL) (writers.ConfigWriter, error) { - if uri.Host != "" { - // prevent the common mistake of "file://path/to/file" - return nil, fmt.Errorf(`invalid file uri %q (did you mean "file:///%s%s"?)`, uri.String(), uri.Host, uri.Path) - } - return &fileWriter{ - filePath: uri.Path, - }, nil - }) + writers.RegisterBuilder("file", newFileWriter) +} + +func newFileWriter(uri *url.URL) (writers.ConfigWriter, error) { + if uri.Host != "" { + // prevent the common mistake of "file://path/to/file" + return nil, fmt.Errorf(`invalid file uri %q (did you mean "file:///%s%s"?)`, uri.String(), uri.Host, uri.Path) + } + return &fileWriter{ + filePath: uri.Path, + }, nil } type fileWriter struct { + opts writers.ConfigWriterOptions filePath string } +// WithOptions implements writers.ConfigWriter. +func (w *fileWriter) WithOptions(opts writers.ConfigWriterOptions) writers.ConfigWriter { + clone := *w + clone.opts = opts + return &clone +} + // WriteConfig implements ConfigWriter. -func (w *fileWriter) WriteConfig(_ context.Context, src *cluster_api.BootstrapConfig, cipher cipher.AEAD) error { - plaintext, err := json.Marshal(src) +func (w *fileWriter) WriteConfig(_ context.Context, src *cluster_api.BootstrapConfig) error { + data, err := json.Marshal(src) if err != nil { return fmt.Errorf("marshal file config: %w", err) } - ciphertext := cryptutil.Encrypt(cipher, plaintext, nil) - err = os.WriteFile(w.filePath, ciphertext, 0o600) + if w.opts.Cipher != nil { + data = cryptutil.Encrypt(w.opts.Cipher, data, nil) + } + err = os.WriteFile(w.filePath, data, 0o600) if err != nil { return fmt.Errorf("write bootstrap config: %w", err) } diff --git a/internal/zero/bootstrap/writers/filesystem/file_test.go b/internal/zero/bootstrap/writers/filesystem/file_test.go index 3c25fe9cf..a4f64fa4f 100644 --- a/internal/zero/bootstrap/writers/filesystem/file_test.go +++ b/internal/zero/bootstrap/writers/filesystem/file_test.go @@ -31,7 +31,10 @@ func TestFileWriter(t *testing.T) { writer, err := writers.NewForURI(fmt.Sprintf("file://%s", fd.Name())) require.NoError(t, err) - require.NoError(t, bootstrap.SaveBootstrapConfig(context.Background(), writer, &src, cipher)) + writer = writer.WithOptions(writers.ConfigWriterOptions{ + Cipher: cipher, + }) + require.NoError(t, bootstrap.SaveBootstrapConfig(context.Background(), writer, &src)) dst, err := bootstrap.LoadBootstrapConfigFromFile(fd.Name(), cipher) require.NoError(t, err) diff --git a/internal/zero/bootstrap/writers/k8s/secret.go b/internal/zero/bootstrap/writers/k8s/secret.go index ef8017d0c..3b2405845 100644 --- a/internal/zero/bootstrap/writers/k8s/secret.go +++ b/internal/zero/bootstrap/writers/k8s/secret.go @@ -1,8 +1,8 @@ package k8s import ( + "bytes" "context" - "crypto/cipher" "crypto/tls" "crypto/x509" "encoding/base64" @@ -21,15 +21,15 @@ import ( "github.com/pomerium/pomerium/internal/zero/bootstrap/writers" "github.com/pomerium/pomerium/pkg/cryptutil" cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" + "gopkg.in/yaml.v3" ) func init() { - writers.RegisterBuilder("secret", func(uri *url.URL) (writers.ConfigWriter, error) { - return newSecretWriter(uri) - }) + writers.RegisterBuilder("secret", newSecretWriter) } type secretWriter struct { + opts writers.ConfigWriterOptions client *http.Client apiserverURL *url.URL namespace string @@ -37,7 +37,14 @@ type secretWriter struct { key string } -func newSecretWriter(uri *url.URL) (*secretWriter, error) { +// WithOptions implements writers.ConfigWriter. +func (w *secretWriter) WithOptions(opts writers.ConfigWriterOptions) writers.ConfigWriter { + clone := *w + clone.opts = opts + return &clone +} + +func newSecretWriter(uri *url.URL) (writers.ConfigWriter, error) { client, apiserverURL, err := inClusterConfig() if err != nil { return nil, err @@ -59,7 +66,7 @@ func newSecretWriter(uri *url.URL) (*secretWriter, error) { } // WriteConfig implements ConfigWriter. -func (w *secretWriter) WriteConfig(ctx context.Context, src *cluster_api.BootstrapConfig, cipher cipher.AEAD) error { +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{ @@ -67,23 +74,29 @@ func (w *secretWriter) WriteConfig(ctx context.Context, src *cluster_api.Bootstr "force": {"true"}, }.Encode(), }) - plaintext, err := json.Marshal(src) + data, err := json.Marshal(src) if err != nil { return err } - ciphertext := cryptutil.Encrypt(cipher, plaintext, nil) - encodedCiphertext := base64.StdEncoding.EncodeToString(ciphertext) - patch := fmt.Sprintf(`--- -apiVersion: v1 -kind: Secret -metadata: - name: %q - namespace: %q -data: - %q: %q -`, w.name, w.namespace, w.key, encodedCiphertext) - req, err := http.NewRequestWithContext(ctx, http.MethodPatch, u.String(), strings.NewReader(patch)) + 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 } @@ -101,11 +114,17 @@ data: if resp.Header.Get("Content-Type") == "application/json" { // log the detailed status message if available status, err := io.ReadAll(resp.Body) - if err != nil && len(status) > 0 { - log.Ctx(ctx).Error(). - RawJSON("response", status). - Msg("forbidden") + 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) diff --git a/internal/zero/bootstrap/writers/k8s/secret_test.go b/internal/zero/bootstrap/writers/k8s/secret_test.go index c94ee62a7..51b9ee69d 100644 --- a/internal/zero/bootstrap/writers/k8s/secret_test.go +++ b/internal/zero/bootstrap/writers/k8s/secret_test.go @@ -70,7 +70,11 @@ func TestInClusterConfig(t *testing.T) { DatabrokerStorageConnection: &txt, } - require.NoError(t, bootstrap.SaveBootstrapConfig(context.Background(), writer, &src, cipher)) + writer := writer.WithOptions(writers.ConfigWriterOptions{ + Cipher: cipher, + }) + + require.NoError(t, bootstrap.SaveBootstrapConfig(context.Background(), writer, &src)) r := <-requests assert.Equal(t, "PATCH", r.Method) @@ -141,7 +145,6 @@ func TestInClusterConfig(t *testing.T) { }, } { for _, uri := range tc.uris { - w, err := writers.NewForURI(uri) if tc.errf == "" { assert.NoError(t, err) diff --git a/internal/zero/bootstrap/writers/writers.go b/internal/zero/bootstrap/writers/writers.go index 5d2cca6ef..3ec7607eb 100644 --- a/internal/zero/bootstrap/writers/writers.go +++ b/internal/zero/bootstrap/writers/writers.go @@ -11,7 +11,14 @@ import ( ) type ConfigWriter interface { - WriteConfig(ctx context.Context, src *cluster_api.BootstrapConfig, cipher cipher.AEAD) error + WriteConfig(ctx context.Context, src *cluster_api.BootstrapConfig) error + WithOptions(opts ConfigWriterOptions) ConfigWriter +} + +type ConfigWriterOptions struct { + // A cipher used to encrypt the configuration before writing it. + // If nil, the configuration will be written in plaintext. + Cipher cipher.AEAD } // A WriterBuilder creates and initializes a new ConfigWriter previously