Compare commits
No commits in common. "6437318f9184136b2119ab45104d3f385aa3e474" and "92ae57121d4af18418a3f9fa26eedfd2b057da2a" have entirely different histories.
6437318f91
...
92ae57121d
|
|
@ -23,8 +23,8 @@ secrets:
|
||||||
platform/cloudflare:
|
platform/cloudflare:
|
||||||
api-token: encrypted-value
|
api-token: encrypted-value
|
||||||
platform/cloudflare-tunnel:
|
platform/cloudflare-tunnel:
|
||||||
credentials: encrypted-value
|
credentials.json: encrypted-value
|
||||||
config: encrypted-value
|
config.yml: encrypted-value
|
||||||
```
|
```
|
||||||
|
|
||||||
Keys are written to OpenBao KV v2 under `secret/<path>`. Additional paths are
|
Keys are written to OpenBao KV v2 under `secret/<path>`. Additional paths are
|
||||||
|
|
@ -53,8 +53,6 @@ go run . cloudflare-tunnel import --config <private-bootstrap-config> --credenti
|
||||||
The import command reads the file only in memory, validates its credential
|
The import command reads the file only in memory, validates its credential
|
||||||
shape, stores only the credentials JSON and terminal-404 local config shown
|
shape, stores only the credentials JSON and terminal-404 local config shown
|
||||||
above, and seeds OpenBao. It never saves a Tunnel run token.
|
above, and seeds OpenBao. It never saves a Tunnel run token.
|
||||||
The template maps these simple OpenBao properties to the `credentials.json` and
|
|
||||||
`config.yml` Kubernetes filenames.
|
|
||||||
|
|
||||||
`cicd/forgejo-webhook.authorization` is required for delivery bootstrap. The
|
`cicd/forgejo-webhook.authorization` is required for delivery bootstrap. The
|
||||||
CLI supplies it as the Forgejo webhook Authorization header and Tekton compares
|
CLI supplies it as the Forgejo webhook Authorization header and Tekton compares
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,6 @@ var writeGeneratedSOPS = writeSOPSEncryptedFile
|
||||||
|
|
||||||
var readCloudflareOperationalSecrets = ReadOperationalSecrets
|
var readCloudflareOperationalSecrets = ReadOperationalSecrets
|
||||||
|
|
||||||
var writeCloudflareOperationalSecrets = WriteOperationalSecrets
|
|
||||||
|
|
||||||
var ensureForgejoWebhook = func(cfg config.Config, repo, webhookURL, authorization string) error {
|
var ensureForgejoWebhook = func(cfg config.Config, repo, webhookURL, authorization string) error {
|
||||||
manager := forgejo.NewRepoManager(cfg.Git.BaseURL, cfg.Git.Token, cfg.Git.Owner, cfg.Git.Username, cfg.Flux.ManifestsRepo, cfg.Flux.RepoName, cfg.Flux.Branch, "maidn/bootstrap-"+cfg.ClusterID)
|
manager := forgejo.NewRepoManager(cfg.Git.BaseURL, cfg.Git.Token, cfg.Git.Owner, cfg.Git.Username, cfg.Flux.ManifestsRepo, cfg.Flux.RepoName, cfg.Flux.Branch, "maidn/bootstrap-"+cfg.ClusterID)
|
||||||
return manager.EnsureWebhook(repo, webhookURL, authorization)
|
return manager.EnsureWebhook(repo, webhookURL, authorization)
|
||||||
|
|
@ -262,19 +260,8 @@ func (r Runner) reconcileCloudflareTunnel() error {
|
||||||
return fmt.Errorf("read encrypted Cloudflare operational state: %w", err)
|
return fmt.Errorf("read encrypted Cloudflare operational state: %w", err)
|
||||||
}
|
}
|
||||||
tunnelState := secrets["platform/cloudflare-tunnel"]
|
tunnelState := secrets["platform/cloudflare-tunnel"]
|
||||||
stored, present, legacy, err := cloudflare.ParseStoredTunnelState(tunnelState)
|
_, present, err := cloudflare.ParseStoredTunnel(tunnelState)
|
||||||
if present {
|
if present {
|
||||||
if !legacy {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
values, err := stored.Values()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
secrets["platform/cloudflare-tunnel"] = values
|
|
||||||
if err := writeCloudflareOperationalSecrets(r.Config.SOPS.OperationalSecretsPath, r.Config.SOPS.AgeKeyPath, secrets); err != nil {
|
|
||||||
return errors.New("save normalized encrypted Cloudflare tunnel state")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err == nil || cloudflare.IsLegacyRunTokenState(tunnelState) {
|
if err == nil || cloudflare.IsLegacyRunTokenState(tunnelState) {
|
||||||
|
|
|
||||||
|
|
@ -812,43 +812,6 @@ func TestReconcileCloudflareTunnelValidatesManagedState(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReconcileCloudflareTunnelMigratesDottedStateAtomically(t *testing.T) {
|
|
||||||
originalRead := readCloudflareOperationalSecrets
|
|
||||||
originalWrite := writeCloudflareOperationalSecrets
|
|
||||||
t.Cleanup(func() {
|
|
||||||
readCloudflareOperationalSecrets = originalRead
|
|
||||||
writeCloudflareOperationalSecrets = originalWrite
|
|
||||||
})
|
|
||||||
values, err := (cloudflare.StoredTunnel{
|
|
||||||
Credentials: cloudflare.Credentials{AccountTag: "account", TunnelSecret: "secret", TunnelID: "tunnel"},
|
|
||||||
Config: cloudflare.NewConfig("tunnel"),
|
|
||||||
}).Values()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
secrets := map[string]map[string]string{
|
|
||||||
"platform/cloudflare-tunnel": {"credentials.json": values["credentials"], "config.yml": values["config"]},
|
|
||||||
"platform/pihole": {"password": "preserved"},
|
|
||||||
}
|
|
||||||
readCloudflareOperationalSecrets = func(string, string) (map[string]map[string]string, error) { return secrets, nil }
|
|
||||||
writes := 0
|
|
||||||
writeCloudflareOperationalSecrets = func(path, ageKeyPath string, updated map[string]map[string]string) error {
|
|
||||||
if path != "secrets" || ageKeyPath != "age" || updated["platform/pihole"]["password"] != "preserved" {
|
|
||||||
t.Fatal("dotted-state migration did not use the encrypted operational state boundary")
|
|
||||||
}
|
|
||||||
secrets = updated
|
|
||||||
writes++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := (Runner{Config: config.Config{SOPS: config.SOPSConfig{OperationalSecretsPath: "secrets", AgeKeyPath: "age"}}}).reconcileCloudflareTunnel(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_, present, legacy, err := cloudflare.ParseStoredTunnelState(secrets["platform/cloudflare-tunnel"])
|
|
||||||
if err != nil || !present || legacy || writes != 1 || len(secrets["platform/cloudflare-tunnel"]) != 2 {
|
|
||||||
t.Fatal("dotted Cloudflare tunnel state was not rewritten to simple keys")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReconcileCloudflareTunnelRequiresImportForAbsentOrLegacyState(t *testing.T) {
|
func TestReconcileCloudflareTunnelRequiresImportForAbsentOrLegacyState(t *testing.T) {
|
||||||
originalRead := readCloudflareOperationalSecrets
|
originalRead := readCloudflareOperationalSecrets
|
||||||
t.Cleanup(func() { readCloudflareOperationalSecrets = originalRead })
|
t.Cleanup(func() { readCloudflareOperationalSecrets = originalRead })
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
credentialsKey = "credentials"
|
credentialsKey = "credentials.json"
|
||||||
configKey = "config"
|
configKey = "config.yml"
|
||||||
legacyCredentialsKey = "credentials.json"
|
|
||||||
legacyConfigKey = "config.yml"
|
|
||||||
credentialsFile = "/etc/cloudflared/credentials.json"
|
credentialsFile = "/etc/cloudflared/credentials.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -79,8 +77,8 @@ func credentialsFromSecret(contents []byte) (Credentials, error) {
|
||||||
if err := decoder.Decode(&secret); err != nil || decoder.Decode(&struct{}{}) != io.EOF || secret.APIVersion != "v1" || secret.Kind != "Secret" {
|
if err := decoder.Decode(&secret); err != nil || decoder.Decode(&struct{}{}) != io.EOF || secret.APIVersion != "v1" || secret.Kind != "Secret" {
|
||||||
return Credentials{}, errors.New("invalid Secret")
|
return Credentials{}, errors.New("invalid Secret")
|
||||||
}
|
}
|
||||||
plaintext, inStringData := secret.StringData[legacyCredentialsKey]
|
plaintext, inStringData := secret.StringData[credentialsKey]
|
||||||
encoded, inData := secret.Data[legacyCredentialsKey]
|
encoded, inData := secret.Data[credentialsKey]
|
||||||
if inStringData == inData {
|
if inStringData == inData {
|
||||||
return Credentials{}, errors.New("missing Secret credentials")
|
return Credentials{}, errors.New("missing Secret credentials")
|
||||||
}
|
}
|
||||||
|
|
@ -107,46 +105,28 @@ func NewRoute(hostname, service string) (Route, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseStoredTunnel(values map[string]string) (StoredTunnel, bool, error) {
|
func ParseStoredTunnel(values map[string]string) (StoredTunnel, bool, error) {
|
||||||
stored, present, _, err := ParseStoredTunnelState(values)
|
|
||||||
return stored, present, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseStoredTunnelState reports whether state uses the legacy dotted keys.
|
|
||||||
func ParseStoredTunnelState(values map[string]string) (StoredTunnel, bool, bool, error) {
|
|
||||||
credentialsJSON, hasCredentials := values[credentialsKey]
|
credentialsJSON, hasCredentials := values[credentialsKey]
|
||||||
configYAML, hasConfig := values[configKey]
|
configYAML, hasConfig := values[configKey]
|
||||||
legacyCredentialsJSON, hasLegacyCredentials := values[legacyCredentialsKey]
|
if !hasCredentials && !hasConfig {
|
||||||
legacyConfigYAML, hasLegacyConfig := values[legacyConfigKey]
|
|
||||||
if hasCredentials || hasConfig {
|
|
||||||
if !hasCredentials || !hasConfig || len(values) != 2 {
|
|
||||||
return StoredTunnel{}, false, false, errors.New("Cloudflare tunnel operational state is ambiguous; expected only credentials and config")
|
|
||||||
}
|
|
||||||
return parseStoredTunnel(credentialsJSON, configYAML, false)
|
|
||||||
}
|
|
||||||
if hasLegacyCredentials || hasLegacyConfig {
|
|
||||||
if !hasLegacyCredentials || !hasLegacyConfig || len(values) != 2 {
|
|
||||||
return StoredTunnel{}, false, false, errors.New("Cloudflare tunnel operational state is ambiguous; expected only credentials and config")
|
|
||||||
}
|
|
||||||
return parseStoredTunnel(legacyCredentialsJSON, legacyConfigYAML, true)
|
|
||||||
}
|
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return StoredTunnel{}, false, false, nil
|
return StoredTunnel{}, false, nil
|
||||||
|
}
|
||||||
|
return StoredTunnel{}, false, errors.New("Cloudflare tunnel operational state is ambiguous; expected credentials.json and config.yml")
|
||||||
|
}
|
||||||
|
if !hasCredentials || !hasConfig || len(values) != 2 {
|
||||||
|
return StoredTunnel{}, false, errors.New("Cloudflare tunnel operational state is ambiguous; expected only credentials.json and config.yml")
|
||||||
}
|
}
|
||||||
return StoredTunnel{}, false, false, errors.New("Cloudflare tunnel operational state is ambiguous; expected credentials and config")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseStoredTunnel(credentialsJSON, configYAML string, legacy bool) (StoredTunnel, bool, bool, error) {
|
|
||||||
credentials, err := parseCredentials([]byte(credentialsJSON))
|
credentials, err := parseCredentials([]byte(credentialsJSON))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StoredTunnel{}, false, false, err
|
return StoredTunnel{}, false, err
|
||||||
}
|
}
|
||||||
var config Config
|
var config Config
|
||||||
yamlDecoder := yaml.NewDecoder(strings.NewReader(configYAML))
|
yamlDecoder := yaml.NewDecoder(strings.NewReader(configYAML))
|
||||||
yamlDecoder.KnownFields(true)
|
yamlDecoder.KnownFields(true)
|
||||||
if err := yamlDecoder.Decode(&config); err != nil || yamlDecoder.Decode(&struct{}{}) != io.EOF || !validConfig(config) || config.Tunnel != credentials.TunnelID {
|
if err := yamlDecoder.Decode(&config); err != nil || yamlDecoder.Decode(&struct{}{}) != io.EOF || !validConfig(config) || config.Tunnel != credentials.TunnelID {
|
||||||
return StoredTunnel{}, false, false, errors.New("Cloudflare tunnel config is invalid")
|
return StoredTunnel{}, false, errors.New("Cloudflare tunnel config is invalid")
|
||||||
}
|
}
|
||||||
return StoredTunnel{Credentials: credentials, Config: config}, true, legacy, nil
|
return StoredTunnel{Credentials: credentials, Config: config}, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCredentials(contents []byte) (Credentials, error) {
|
func parseCredentials(contents []byte) (Credentials, error) {
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,10 @@ func TestStoredTunnelStartsWithoutPublicIngress(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, hasCredentials := values["credentials"]; !hasCredentials {
|
|
||||||
t.Fatal("new tunnel state did not use a simple credentials key")
|
|
||||||
}
|
|
||||||
parsed, present, err := ParseStoredTunnel(values)
|
parsed, present, err := ParseStoredTunnel(values)
|
||||||
if err != nil || !present || len(parsed.Config.Ingress) != 1 || parsed.Config.Ingress[0].Service != "http_status:404" {
|
if err != nil || !present || len(parsed.Config.Ingress) != 1 || parsed.Config.Ingress[0].Service != "http_status:404" {
|
||||||
t.Fatal("new tunnel state must contain only the terminal ingress rule")
|
t.Fatal("new tunnel state must contain only the terminal ingress rule")
|
||||||
}
|
}
|
||||||
legacy := map[string]string{"credentials.json": values["credentials"], "config.yml": values["config"]}
|
|
||||||
_, present, isLegacy, err := ParseStoredTunnelState(legacy)
|
|
||||||
if err != nil || !present || !isLegacy {
|
|
||||||
t.Fatal("valid dotted tunnel state was not recognized for migration")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStoredTunnelRejectsLegacyOrPartialState(t *testing.T) {
|
func TestStoredTunnelRejectsLegacyOrPartialState(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue