1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package clusterstack
import (
"errors"
"fmt"
"net"
"net/url"
"path/filepath"
"strconv"
"strings"
)
const (
OpenBaoDirName = "openbao"
OpenBaoStorageDirName = "data"
OpenBaoConfigFileName = "openbao.hcl"
openBaoLoopbackHost = "127.0.0.1"
OpenBaoDatabase = "openbao"
openBaoConfigTemplate = `ui = false
disable_clustering = true
api_addr = "https://%[1]s"
listener "tcp" {
address = "%[1]s"
tls_cert_file = "%[2]s"
tls_key_file = "%[3]s"
}
storage "postgresql" {
connection_url = "%[4]s"
}
`
)
var ErrNoOpenBaoTLSCertificate = errors.New(
"clusterstack: the vault listener is loopback-only and TLS, so it needs the per-cluster " +
"server certificate and key. A listener without them comes up as an ACME/plaintext " +
"listener: a guest that dials the gateway address through pasta, and the admin that " +
"dials loopback, both need certificate auth to be presentable in the handshake")
var ErrNoOpenBaoPort = errors.New(
"clusterstack: Spec.OpenBaoPort is required. bao takes no port on its command line, so a " +
"config that names no address leaves the vault on its own built-in 8200 -- one port for " +
"the whole host. The first cluster's vault binds it, every later cluster's vault dies at " +
"bind, and the file that produced the failure is byte-identical for both clusters")
var ErrOpenBaoDataDirNotAbsolute = errors.New(
"clusterstack: Spec.DataDir must be absolute before an OpenBao config can be rendered from " +
"it. Every path in that config is derived from DataDir, so a relative one resolves against " +
"whichever directory the gateway happened to start in: two clusters started from one " +
"directory are handed one vault's storage and one vault's seal, and a gateway restarted " +
"from another directory brings the vault up on an empty store")
var ErrOpenBaoConfigNotPerCluster = errors.New(
"clusterstack: Spec.OpenBaoConfig must name a file inside Spec.DataDir. A config shared " +
"between clusters is written by whichever cluster reconciled last, so the listener port " +
"and the storage path in it are that cluster's: the other cluster's vault comes up holding " +
"a port its own leases do not cover and a store that is not its own")
var ErrNoOpenBaoStore = errors.New(
"clusterstack: Spec.OpenBaoPostgresDSN is required. The vault's store is this cluster's own " +
"embedded-postgres, never a defaulted one: a vault that names no store, or one shared with " +
"another cluster, comes up on a store that is not its own, and the store holds the vault's " +
"seal and its data, so the wrong store is a vault that cannot unseal the material it holds")
var ErrOpenBaoStoreNotPerCluster = errors.New(
"clusterstack: Spec.OpenBaoPostgresDSN must name this cluster's own postgres database " +
"(\"" + OpenBaoDatabase + "\") over the unix socket of that cluster's Postgres object. A store shared with " +
"the objects kine writes is one database two owners write to, and a store that is another " +
"cluster's is a vault whose unseal material does not belong to it")
func OpenBaoDir(dataDir string) string {
return filepath.Join(dataDir, OpenBaoDirName)
}
func OpenBaoStorageDir(dataDir string) string {
return filepath.Join(OpenBaoDir(dataDir), OpenBaoStorageDirName)
}
func OpenBaoConfigPath(dataDir string) string {
return filepath.Join(dataDir, OpenBaoConfigFileName)
}
func (s Spec) openBaoAddress() (string, error) {
if s.OpenBaoPort <= 0 || s.OpenBaoPort > 65535 {
return "", fmt.Errorf("%w (got %d)", ErrNoOpenBaoPort, s.OpenBaoPort)
}
return net.JoinHostPort(openBaoLoopbackHost, strconv.Itoa(s.OpenBaoPort)), nil
}
func isPerCluster(dataDir, path string) bool {
rel, err := filepath.Rel(dataDir, path)
if err != nil {
return false
}
if filepath.IsAbs(rel) || rel == "." {
return false
}
return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
func (s Spec) OpenBaoServerConfig() (string, error) {
if !filepath.IsAbs(s.DataDir) {
return "", fmt.Errorf("%w (got %q)", ErrOpenBaoDataDirNotAbsolute, s.DataDir)
}
if !isPerCluster(s.DataDir, s.OpenBaoConfig) {
return "", fmt.Errorf("%w (config %q, data dir %q)", ErrOpenBaoConfigNotPerCluster,
s.OpenBaoConfig, s.DataDir)
}
address, err := s.openBaoAddress()
if err != nil {
return "", err
}
if s.OpenBaoTLSCertFile == "" || s.OpenBaoTLSKeyFile == "" {
return "", fmt.Errorf("%w (cert %q, key %q)", ErrNoOpenBaoTLSCertificate,
s.OpenBaoTLSCertFile, s.OpenBaoTLSKeyFile)
}
dsn, err := openBaoStoreDSN(s.OpenBaoPostgresDSN)
if err != nil {
return "", err
}
return fmt.Sprintf(openBaoConfigTemplate, address, s.OpenBaoTLSCertFile, s.OpenBaoTLSKeyFile,
dsn), nil
}
func openBaoStoreDSN(dsn string) (string, error) {
if dsn == "" {
return "", ErrNoOpenBaoStore
}
if strings.ContainsAny(dsn, "\"\n\r") {
return "", fmt.Errorf("%w (%q carries a character that would end the HCL string)",
ErrOpenBaoStoreNotPerCluster, dsn)
}
u, err := url.Parse(dsn)
if err != nil {
return "", fmt.Errorf("%w (parsing %q: %v)", ErrOpenBaoStoreNotPerCluster, dsn, err)
}
switch u.Scheme {
case "postgres", "postgresql":
default:
return "", fmt.Errorf("%w (scheme %q, want postgres)", ErrOpenBaoStoreNotPerCluster, u.Scheme)
}
if socket := u.Query().Get("host"); !strings.HasPrefix(socket, "/") {
return "", fmt.Errorf("%w (%q names no unix socket directory in its host parameter)", ErrOpenBaoStoreNotPerCluster, dsn)
}
if u.User == nil || u.User.Username() == "" {
return "", fmt.Errorf("%w (%q carries no user)", ErrOpenBaoStoreNotPerCluster, dsn)
}
if strings.TrimPrefix(u.Path, "/") != OpenBaoDatabase {
return "", fmt.Errorf("%w (%q names database %q)", ErrOpenBaoStoreNotPerCluster, dsn,
strings.TrimPrefix(u.Path, "/"))
}
return dsn, nil
}