1/*
2Package config provides a way to find and load SOPS configuration files
3*/
4package config //import "go.mozilla.org/sops/v3/config"
5
6import (
7	"fmt"
8	"io/ioutil"
9	"os"
10	"path"
11	"regexp"
12
13	"github.com/mozilla-services/yaml"
14	"github.com/sirupsen/logrus"
15	"go.mozilla.org/sops/v3"
16	"go.mozilla.org/sops/v3/azkv"
17	"go.mozilla.org/sops/v3/gcpkms"
18	"go.mozilla.org/sops/v3/hcvault"
19	"go.mozilla.org/sops/v3/kms"
20	"go.mozilla.org/sops/v3/logging"
21	"go.mozilla.org/sops/v3/pgp"
22	"go.mozilla.org/sops/v3/publish"
23)
24
25var log *logrus.Logger
26
27func init() {
28	log = logging.NewLogger("CONFIG")
29}
30
31type fileSystem interface {
32	Stat(name string) (os.FileInfo, error)
33}
34
35type osFS struct {
36	stat func(string) (os.FileInfo, error)
37}
38
39func (fs osFS) Stat(name string) (os.FileInfo, error) {
40	return fs.stat(name)
41}
42
43var fs fileSystem = osFS{stat: os.Stat}
44
45const (
46	maxDepth       = 100
47	configFileName = ".sops.yaml"
48)
49
50// FindConfigFile looks for a sops config file in the current working directory and on parent directories, up to the limit defined by the maxDepth constant.
51func FindConfigFile(start string) (string, error) {
52	filepath := path.Dir(start)
53	for i := 0; i < maxDepth; i++ {
54		_, err := fs.Stat(path.Join(filepath, configFileName))
55		if err != nil {
56			filepath = path.Join(filepath, "..")
57		} else {
58			return path.Join(filepath, configFileName), nil
59		}
60	}
61	return "", fmt.Errorf("Config file not found")
62}
63
64type configFile struct {
65	CreationRules    []creationRule    `yaml:"creation_rules"`
66	DestinationRules []destinationRule `yaml:"destination_rules"`
67}
68
69type keyGroup struct {
70	KMS     []kmsKey
71	GCPKMS  []gcpKmsKey  `yaml:"gcp_kms"`
72	AzureKV []azureKVKey `yaml:"azure_keyvault"`
73	Vault   []string     `yaml:"hc_vault"`
74	PGP     []string
75}
76
77type gcpKmsKey struct {
78	ResourceID string `yaml:"resource_id"`
79}
80
81type kmsKey struct {
82	Arn        string             `yaml:"arn"`
83	Role       string             `yaml:"role,omitempty"`
84	Context    map[string]*string `yaml:"context"`
85	AwsProfile string             `yaml:"aws_profile"`
86}
87
88type azureKVKey struct {
89	VaultURL string `yaml:"vaultUrl"`
90	Key      string `yaml:"key"`
91	Version  string `yaml:"version"`
92}
93
94type destinationRule struct {
95	PathRegex        string       `yaml:"path_regex"`
96	S3Bucket         string       `yaml:"s3_bucket"`
97	S3Prefix         string       `yaml:"s3_prefix"`
98	GCSBucket        string       `yaml:"gcs_bucket"`
99	GCSPrefix        string       `yaml:"gcs_prefix"`
100	VaultPath        string       `yaml:"vault_path"`
101	VaultAddress     string       `yaml:"vault_address"`
102	VaultKVMountName string       `yaml:"vault_kv_mount_name"`
103	VaultKVVersion   int          `yaml:"vault_kv_version"`
104	RecreationRule   creationRule `yaml:"recreation_rule,omitempty"`
105	OmitExtensions   bool         `yaml:"omit_extensions"`
106}
107
108type creationRule struct {
109	PathRegex         string `yaml:"path_regex"`
110	KMS               string
111	AwsProfile        string `yaml:"aws_profile"`
112	PGP               string
113	GCPKMS            string     `yaml:"gcp_kms"`
114	AzureKeyVault     string     `yaml:"azure_keyvault"`
115	VaultURI          string     `yaml:"hc_vault_transit_uri"`
116	KeyGroups         []keyGroup `yaml:"key_groups"`
117	ShamirThreshold   int        `yaml:"shamir_threshold"`
118	UnencryptedSuffix string     `yaml:"unencrypted_suffix"`
119	EncryptedSuffix   string     `yaml:"encrypted_suffix"`
120	UnencryptedRegex  string     `yaml:"unencrypted_regex"`
121	EncryptedRegex    string     `yaml:"encrypted_regex"`
122}
123
124// Load loads a sops config file into a temporary struct
125func (f *configFile) load(bytes []byte) error {
126	err := yaml.Unmarshal(bytes, f)
127	if err != nil {
128		return fmt.Errorf("Could not unmarshal config file: %s", err)
129	}
130	return nil
131}
132
133// Config is the configuration for a given SOPS file
134type Config struct {
135	KeyGroups         []sops.KeyGroup
136	ShamirThreshold   int
137	UnencryptedSuffix string
138	EncryptedSuffix   string
139	UnencryptedRegex  string
140	EncryptedRegex    string
141	Destination       publish.Destination
142	OmitExtensions    bool
143}
144
145func getKeyGroupsFromCreationRule(cRule *creationRule, kmsEncryptionContext map[string]*string) ([]sops.KeyGroup, error) {
146	var groups []sops.KeyGroup
147	if len(cRule.KeyGroups) > 0 {
148		for _, group := range cRule.KeyGroups {
149			var keyGroup sops.KeyGroup
150			for _, k := range group.PGP {
151				keyGroup = append(keyGroup, pgp.NewMasterKeyFromFingerprint(k))
152			}
153			for _, k := range group.KMS {
154				keyGroup = append(keyGroup, kms.NewMasterKey(k.Arn, k.Role, k.Context))
155			}
156			for _, k := range group.GCPKMS {
157				keyGroup = append(keyGroup, gcpkms.NewMasterKeyFromResourceID(k.ResourceID))
158			}
159			for _, k := range group.AzureKV {
160				keyGroup = append(keyGroup, azkv.NewMasterKey(k.VaultURL, k.Key, k.Version))
161			}
162			for _, k := range group.Vault {
163				if masterKey, err := hcvault.NewMasterKeyFromURI(k); err == nil {
164					keyGroup = append(keyGroup, masterKey)
165				} else {
166					return nil, err
167				}
168			}
169			groups = append(groups, keyGroup)
170		}
171	} else {
172		var keyGroup sops.KeyGroup
173		for _, k := range pgp.MasterKeysFromFingerprintString(cRule.PGP) {
174			keyGroup = append(keyGroup, k)
175		}
176		for _, k := range kms.MasterKeysFromArnString(cRule.KMS, kmsEncryptionContext, cRule.AwsProfile) {
177			keyGroup = append(keyGroup, k)
178		}
179		for _, k := range gcpkms.MasterKeysFromResourceIDString(cRule.GCPKMS) {
180			keyGroup = append(keyGroup, k)
181		}
182		azureKeys, err := azkv.MasterKeysFromURLs(cRule.AzureKeyVault)
183		if err != nil {
184			return nil, err
185		}
186		for _, k := range azureKeys {
187			keyGroup = append(keyGroup, k)
188		}
189		vaultKeys, err := hcvault.NewMasterKeysFromURIs(cRule.VaultURI)
190		if err != nil {
191			return nil, err
192		}
193		for _, k := range vaultKeys {
194			keyGroup = append(keyGroup, k)
195		}
196		groups = append(groups, keyGroup)
197	}
198	return groups, nil
199}
200
201func loadConfigFile(confPath string) (*configFile, error) {
202	confBytes, err := ioutil.ReadFile(confPath)
203	if err != nil {
204		return nil, fmt.Errorf("could not read config file: %s", err)
205	}
206	conf := &configFile{}
207	err = conf.load(confBytes)
208	if err != nil {
209		return nil, fmt.Errorf("error loading config: %s", err)
210	}
211	return conf, nil
212}
213
214func configFromRule(rule *creationRule, kmsEncryptionContext map[string]*string) (*Config, error) {
215	cryptRuleCount := 0
216	if rule.UnencryptedSuffix != "" {
217		cryptRuleCount++
218	}
219	if rule.EncryptedSuffix != "" {
220		cryptRuleCount++
221	}
222	if rule.EncryptedRegex != "" {
223		cryptRuleCount++
224	}
225
226	if cryptRuleCount > 1 {
227		return nil, fmt.Errorf("error loading config: cannot use more than one of encrypted_suffix, unencrypted_suffix, or encrypted_regex for the same rule")
228	}
229
230	groups, err := getKeyGroupsFromCreationRule(rule, kmsEncryptionContext)
231	if err != nil {
232		return nil, err
233	}
234
235	return &Config{
236		KeyGroups:         groups,
237		ShamirThreshold:   rule.ShamirThreshold,
238		UnencryptedSuffix: rule.UnencryptedSuffix,
239		EncryptedSuffix:   rule.EncryptedSuffix,
240		UnencryptedRegex:  rule.UnencryptedRegex,
241		EncryptedRegex:    rule.EncryptedRegex,
242	}, nil
243}
244
245func parseDestinationRuleForFile(conf *configFile, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
246	var rule *creationRule
247	var dRule *destinationRule
248
249	if len(conf.DestinationRules) > 0 {
250		for _, r := range conf.DestinationRules {
251			if r.PathRegex == "" {
252				dRule = &r
253				rule = &dRule.RecreationRule
254				break
255			}
256			if r.PathRegex != "" {
257				if match, _ := regexp.MatchString(r.PathRegex, filePath); match {
258					dRule = &r
259					rule = &dRule.RecreationRule
260					break
261				}
262			}
263		}
264	}
265
266	if dRule == nil {
267		return nil, fmt.Errorf("error loading config: no matching destination found in config")
268	}
269
270	var dest publish.Destination
271	if dRule != nil {
272		if dRule.S3Bucket != "" && dRule.GCSBucket != "" && dRule.VaultPath != "" {
273			return nil, fmt.Errorf("error loading config: more than one destinations were found in a single destination rule, you can only use one per rule")
274		}
275		if dRule.S3Bucket != "" {
276			dest = publish.NewS3Destination(dRule.S3Bucket, dRule.S3Prefix)
277		}
278		if dRule.GCSBucket != "" {
279			dest = publish.NewGCSDestination(dRule.GCSBucket, dRule.GCSPrefix)
280		}
281		if dRule.VaultPath != "" {
282			dest = publish.NewVaultDestination(dRule.VaultAddress, dRule.VaultPath, dRule.VaultKVMountName, dRule.VaultKVVersion)
283		}
284	}
285
286	config, err := configFromRule(rule, kmsEncryptionContext)
287	if err != nil {
288		return nil, err
289	}
290	config.Destination = dest
291	config.OmitExtensions = dRule.OmitExtensions
292
293	return config, nil
294}
295
296func parseCreationRuleForFile(conf *configFile, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
297	// If config file doesn't contain CreationRules (it's empty or only contains DestionationRules), assume it does not exist
298	if conf.CreationRules == nil {
299		return nil, nil
300	}
301
302	var rule *creationRule
303
304	for _, r := range conf.CreationRules {
305		if r.PathRegex == "" {
306			rule = &r
307			break
308		}
309		if r.PathRegex != "" {
310			if match, _ := regexp.MatchString(r.PathRegex, filePath); match {
311				rule = &r
312				break
313			}
314		}
315	}
316
317	if rule == nil {
318		return nil, fmt.Errorf("error loading config: no matching creation rules found")
319	}
320
321	config, err := configFromRule(rule, kmsEncryptionContext)
322	if err != nil {
323		return nil, err
324	}
325
326	return config, nil
327}
328
329// LoadCreationRuleForFile load the configuration for a given SOPS file from the config file at confPath. A kmsEncryptionContext
330// should be provided for configurations that do not contain key groups, as there's no way to specify context inside
331// a SOPS config file outside of key groups.
332func LoadCreationRuleForFile(confPath string, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
333	conf, err := loadConfigFile(confPath)
334	if err != nil {
335		return nil, err
336	}
337	return parseCreationRuleForFile(conf, filePath, kmsEncryptionContext)
338}
339
340// LoadDestinationRuleForFile works the same as LoadCreationRuleForFile, but gets the "creation_rule" from the matching destination_rule's
341// "recreation_rule".
342func LoadDestinationRuleForFile(confPath string, filePath string, kmsEncryptionContext map[string]*string) (*Config, error) {
343	conf, err := loadConfigFile(confPath)
344	if err != nil {
345		return nil, err
346	}
347	return parseDestinationRuleForFile(conf, filePath, kmsEncryptionContext)
348}
349