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