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.
This commit is contained in:
Joe Kralicky 2024-05-28 15:17:08 -04:00
parent 2177cef346
commit ae5daafbc8
No known key found for this signature in database
GPG key ID: 75C4875F34A9FB79
8 changed files with 93 additions and 44 deletions

View file

@ -104,7 +104,7 @@ func (svc *Source) updateAndSave(ctx context.Context) error {
return nil return nil
} }
err = SaveBootstrapConfig(ctx, svc.writer, cfg, svc.fileCipher) err = SaveBootstrapConfig(ctx, svc.writer, cfg)
if err != nil { if err != nil {
log.Ctx(ctx).Error().Err(err). 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") Msg("failed to save bootstrap config to file, note it may prevent Pomerium from starting up in case of connectivity issues")

View file

@ -42,8 +42,8 @@ func LoadBootstrapConfigFromFile(fp string, cipher cipher.AEAD) (*cluster_api.Bo
} }
// SaveBootstrapConfig saves the bootstrap configuration to a file. // SaveBootstrapConfig saves the bootstrap configuration to a file.
func SaveBootstrapConfig(ctx context.Context, writer writers.ConfigWriter, src *cluster_api.BootstrapConfig, cipher cipher.AEAD) error { func SaveBootstrapConfig(ctx context.Context, writer writers.ConfigWriter, src *cluster_api.BootstrapConfig) error {
err := writer.WriteConfig(ctx, src, cipher) err := writer.WriteConfig(ctx, src)
if err != nil { if err != nil {
health.ReportError(health.ZeroBootstrapConfigSave, err) health.ReportError(health.ZeroBootstrapConfigSave, err)
} else { } else {

View file

@ -55,6 +55,12 @@ func New(secret []byte, fileCachePath *string, writer writers.ConfigWriter, api
return nil, fmt.Errorf("init cypher: %w", err) return nil, fmt.Errorf("init cypher: %w", err)
} }
if writer != nil {
writer = writer.WithOptions(writers.ConfigWriterOptions{
Cipher: cipher,
})
}
svc := &Source{ svc := &Source{
api: api, api: api,
source: source{ready: make(chan struct{})}, source: source{ready: make(chan struct{})},

View file

@ -2,7 +2,6 @@ package filesystem
import ( import (
"context" "context"
"crypto/cipher"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url" "net/url"
@ -14,30 +13,42 @@ import (
) )
func init() { func init() {
writers.RegisterBuilder("file", func(uri *url.URL) (writers.ConfigWriter, error) { writers.RegisterBuilder("file", newFileWriter)
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) func newFileWriter(uri *url.URL) (writers.ConfigWriter, error) {
} if uri.Host != "" {
return &fileWriter{ // prevent the common mistake of "file://path/to/file"
filePath: uri.Path, return nil, fmt.Errorf(`invalid file uri %q (did you mean "file:///%s%s"?)`, uri.String(), uri.Host, uri.Path)
}, nil }
}) return &fileWriter{
filePath: uri.Path,
}, nil
} }
type fileWriter struct { type fileWriter struct {
opts writers.ConfigWriterOptions
filePath string 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. // WriteConfig implements ConfigWriter.
func (w *fileWriter) WriteConfig(_ context.Context, src *cluster_api.BootstrapConfig, cipher cipher.AEAD) error { func (w *fileWriter) WriteConfig(_ context.Context, src *cluster_api.BootstrapConfig) error {
plaintext, err := json.Marshal(src) data, err := json.Marshal(src)
if err != nil { if err != nil {
return fmt.Errorf("marshal file config: %w", err) return fmt.Errorf("marshal file config: %w", err)
} }
ciphertext := cryptutil.Encrypt(cipher, plaintext, nil) if w.opts.Cipher != nil {
err = os.WriteFile(w.filePath, ciphertext, 0o600) data = cryptutil.Encrypt(w.opts.Cipher, data, nil)
}
err = os.WriteFile(w.filePath, data, 0o600)
if err != nil { if err != nil {
return fmt.Errorf("write bootstrap config: %w", err) return fmt.Errorf("write bootstrap config: %w", err)
} }

View file

@ -31,7 +31,10 @@ func TestFileWriter(t *testing.T) {
writer, err := writers.NewForURI(fmt.Sprintf("file://%s", fd.Name())) writer, err := writers.NewForURI(fmt.Sprintf("file://%s", fd.Name()))
require.NoError(t, err) 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) dst, err := bootstrap.LoadBootstrapConfigFromFile(fd.Name(), cipher)
require.NoError(t, err) require.NoError(t, err)

View file

@ -1,8 +1,8 @@
package k8s package k8s
import ( import (
"bytes"
"context" "context"
"crypto/cipher"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
@ -21,15 +21,15 @@ import (
"github.com/pomerium/pomerium/internal/zero/bootstrap/writers" "github.com/pomerium/pomerium/internal/zero/bootstrap/writers"
"github.com/pomerium/pomerium/pkg/cryptutil" "github.com/pomerium/pomerium/pkg/cryptutil"
cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster" cluster_api "github.com/pomerium/pomerium/pkg/zero/cluster"
"gopkg.in/yaml.v3"
) )
func init() { func init() {
writers.RegisterBuilder("secret", func(uri *url.URL) (writers.ConfigWriter, error) { writers.RegisterBuilder("secret", newSecretWriter)
return newSecretWriter(uri)
})
} }
type secretWriter struct { type secretWriter struct {
opts writers.ConfigWriterOptions
client *http.Client client *http.Client
apiserverURL *url.URL apiserverURL *url.URL
namespace string namespace string
@ -37,7 +37,14 @@ type secretWriter struct {
key string 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() client, apiserverURL, err := inClusterConfig()
if err != nil { if err != nil {
return nil, err return nil, err
@ -59,7 +66,7 @@ func newSecretWriter(uri *url.URL) (*secretWriter, error) {
} }
// WriteConfig implements ConfigWriter. // 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{ u := w.apiserverURL.ResolveReference(&url.URL{
Path: path.Join("/api/v1/namespaces", w.namespace, "secrets", w.name), Path: path.Join("/api/v1/namespaces", w.namespace, "secrets", w.name),
RawQuery: url.Values{ RawQuery: url.Values{
@ -67,23 +74,29 @@ func (w *secretWriter) WriteConfig(ctx context.Context, src *cluster_api.Bootstr
"force": {"true"}, "force": {"true"},
}.Encode(), }.Encode(),
}) })
plaintext, err := json.Marshal(src) data, err := json.Marshal(src)
if err != nil { if err != nil {
return err return err
} }
ciphertext := cryptutil.Encrypt(cipher, plaintext, nil)
encodedCiphertext := base64.StdEncoding.EncodeToString(ciphertext)
patch := fmt.Sprintf(`--- if w.opts.Cipher != nil {
apiVersion: v1 data = cryptutil.Encrypt(w.opts.Cipher, data, nil)
kind: Secret }
metadata: encodedData := base64.StdEncoding.EncodeToString(data)
name: %q
namespace: %q patch, _ := yaml.Marshal(map[string]any{
data: "apiVersion": "v1",
%q: %q "kind": "Secret",
`, w.name, w.namespace, w.key, encodedCiphertext) "metadata": map[string]any{
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, u.String(), strings.NewReader(patch)) "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 { if err != nil {
return err return err
} }
@ -101,11 +114,17 @@ data:
if resp.Header.Get("Content-Type") == "application/json" { if resp.Header.Get("Content-Type") == "application/json" {
// log the detailed status message if available // log the detailed status message if available
status, err := io.ReadAll(resp.Body) status, err := io.ReadAll(resp.Body)
if err != nil && len(status) > 0 { if err != nil {
log.Ctx(ctx).Error(). break
RawJSON("response", status).
Msg("forbidden")
} }
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) return fmt.Errorf("unexpected status: %s", resp.Status)

View file

@ -70,7 +70,11 @@ func TestInClusterConfig(t *testing.T) {
DatabrokerStorageConnection: &txt, 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 r := <-requests
assert.Equal(t, "PATCH", r.Method) assert.Equal(t, "PATCH", r.Method)
@ -141,7 +145,6 @@ func TestInClusterConfig(t *testing.T) {
}, },
} { } {
for _, uri := range tc.uris { for _, uri := range tc.uris {
w, err := writers.NewForURI(uri) w, err := writers.NewForURI(uri)
if tc.errf == "" { if tc.errf == "" {
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -11,7 +11,14 @@ import (
) )
type ConfigWriter interface { 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 // A WriterBuilder creates and initializes a new ConfigWriter previously