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
}
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")

View file

@ -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 {

View file

@ -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{})},

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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