Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SOPS: Secrets OPerationS
========================

**SOPS** is an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY
**SOPS** is an editor of encrypted files that supports YAML, JSON, ENV, INI, HCL and BINARY
formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS, age, and PGP.
(`demo <https://www.youtube.com/watch?v=YTEVyLXFiq0>`_)

Expand Down
6 changes: 6 additions & 0 deletions cmd/sops/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/getsops/sops/v3/keyservice"
"github.com/getsops/sops/v3/kms"
"github.com/getsops/sops/v3/stores/dotenv"
"github.com/getsops/sops/v3/stores/hcl"
"github.com/getsops/sops/v3/stores/ini"
"github.com/getsops/sops/v3/stores/json"
"github.com/getsops/sops/v3/stores/yaml"
Expand Down Expand Up @@ -47,6 +48,10 @@ func newDotenvStore(c *config.StoresConfig) Store {
return dotenv.NewStore(&c.Dotenv)
}

func newHclStore(c *config.StoresConfig) Store {
return hcl.NewStore(&c.HCL)
}

func newIniStore(c *config.StoresConfig) Store {
return ini.NewStore(&c.INI)
}
Expand All @@ -62,6 +67,7 @@ func newYamlStore(c *config.StoresConfig) Store {
var storeConstructors = map[Format]storeConstructor{
Binary: newBinaryStore,
Dotenv: newDotenvStore,
Hcl: newHclStore,
Ini: newIniStore,
Json: newJsonStore,
Yaml: newYamlStore,
Expand Down
9 changes: 9 additions & 0 deletions cmd/sops/formats/formats.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Format int
const (
Binary Format = iota
Dotenv
Hcl
Ini
Json
Yaml
Expand All @@ -16,6 +17,7 @@ const (
var stringToFormat = map[string]Format{
"binary": Binary,
"dotenv": Dotenv,
"hcl": Hcl,
"ini": Ini,
"json": Json,
"yaml": Yaml,
Expand Down Expand Up @@ -46,6 +48,11 @@ func IsEnvFile(path string) bool {
return strings.HasSuffix(path, ".env")
}

// IsHclFile returns true if a given file path corresponds to a HCL file
func IsHclFile(path string) bool {
return strings.HasSuffix(path, ".hcl") || strings.HasSuffix(path, ".tfvars")
}

// IsIniFile returns true if a given file path corresponds to a INI file
func IsIniFile(path string) bool {
return strings.HasSuffix(path, ".ini")
Expand All @@ -62,6 +69,8 @@ func FormatForPath(path string) Format {
format = Dotenv
} else if IsIniFile(path) {
format = Ini
} else if IsHclFile(path) {
format = Hcl
}
return format
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/sops/formats/formats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
func TestFormatFromString(t *testing.T) {
assert.Equal(t, Binary, FormatFromString("foobar"))
assert.Equal(t, Dotenv, FormatFromString("dotenv"))
assert.Equal(t, Hcl, FormatFromString("hcl"))
assert.Equal(t, Ini, FormatFromString("ini"))
assert.Equal(t, Yaml, FormatFromString("yaml"))
assert.Equal(t, Json, FormatFromString("json"))
Expand All @@ -17,6 +18,9 @@ func TestFormatFromString(t *testing.T) {
func TestFormatForPath(t *testing.T) {
assert.Equal(t, Binary, FormatForPath("/path/to/foobar"))
assert.Equal(t, Dotenv, FormatForPath("/path/to/foobar.env"))
assert.Equal(t, Hcl, FormatForPath("/path/to/foobar.hcl"))
assert.Equal(t, Hcl, FormatForPath("/path/to/foobar.tfvars"))
assert.Equal(t, Json, FormatForPath("/path/to/foobar.tfvars.json"))
assert.Equal(t, Ini, FormatForPath("/path/to/foobar.ini"))
assert.Equal(t, Json, FormatForPath("/path/to/foobar.json"))
assert.Equal(t, Yaml, FormatForPath("/path/to/foobar.yml"))
Expand All @@ -27,6 +31,8 @@ func TestFormatForPathOrString(t *testing.T) {
assert.Equal(t, Binary, FormatForPathOrString("/path/to/foobar", ""))
assert.Equal(t, Dotenv, FormatForPathOrString("/path/to/foobar", "dotenv"))
assert.Equal(t, Dotenv, FormatForPathOrString("/path/to/foobar.env", ""))
assert.Equal(t, Hcl, FormatForPathOrString("/path/to/foobar", "hcl"))
assert.Equal(t, Hcl, FormatForPathOrString("/path/to/foobar.tfvars", ""))
assert.Equal(t, Ini, FormatForPathOrString("/path/to/foobar", "ini"))
assert.Equal(t, Ini, FormatForPathOrString("/path/to/foobar.ini", ""))
assert.Equal(t, Json, FormatForPathOrString("/path/to/foobar", "json"))
Expand Down
36 changes: 18 additions & 18 deletions cmd/sops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,11 @@ func main() {
},
cli.StringFlag{
Name: "input-type",
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently ini, json, yaml, dotenv, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently ini, json, yaml, dotenv, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.StringFlag{
Name: "filename",
Expand Down Expand Up @@ -517,7 +517,7 @@ func main() {
Flags: []cli.Flag{
cli.StringFlag{
Name: "input-type",
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently ini, json, yaml, dotenv, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
},
Action: func(c *cli.Context) error {
Expand Down Expand Up @@ -732,7 +732,7 @@ func main() {
},
cli.StringFlag{
Name: "input-type",
Usage: "currently ini, json, yaml, dotenv and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently ini, json, yaml, dotenv, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
}, keyserviceFlags...),
Action: func(c *cli.Context) error {
Expand Down Expand Up @@ -801,11 +801,11 @@ func main() {
},
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.BoolFlag{
Name: "ignore-mac",
Expand Down Expand Up @@ -972,11 +972,11 @@ func main() {
},
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.StringFlag{
Name: "unencrypted-suffix",
Expand Down Expand Up @@ -1125,11 +1125,11 @@ func main() {
},
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.StringFlag{
Name: "encryption-context",
Expand Down Expand Up @@ -1331,11 +1331,11 @@ func main() {
},
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.StringFlag{
Name: "unencrypted-suffix",
Expand Down Expand Up @@ -1457,11 +1457,11 @@ func main() {
Flags: append([]cli.Flag{
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.BoolFlag{
Name: "value-file",
Expand Down Expand Up @@ -1593,11 +1593,11 @@ func main() {
Flags: append([]cli.Flag{
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.IntFlag{
Name: "shamir-secret-sharing-threshold",
Expand Down Expand Up @@ -1752,11 +1752,11 @@ func main() {
},
cli.StringFlag{
Name: "input-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the file's extension to determine the type",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the file's extension to determine the type",
},
cli.StringFlag{
Name: "output-type",
Usage: "currently json, yaml, dotenv, ini and binary are supported. If not set, sops will use the input file's extension to determine the output format",
Usage: "currently json, yaml, dotenv, ini, hcl and binary are supported. If not set, sops will use the input file's extension to determine the output format",
},
cli.BoolFlag{
Name: "show-master-keys, s",
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ func FindConfigFile(start string) (string, error) {

type DotenvStoreConfig struct{}

type HCLStoreConfig struct{}

type INIStoreConfig struct{}

type JSONStoreConfig struct {
Expand All @@ -117,6 +119,7 @@ type YAMLStoreConfig struct {

type StoresConfig struct {
Dotenv DotenvStoreConfig `yaml:"dotenv"`
HCL HCLStoreConfig `yaml:"hcl"`
INI INIStoreConfig `yaml:"ini"`
JSONBinary JSONBinaryStoreConfig `yaml:"json_binary"`
JSON JSONStoreConfig `yaml:"json"`
Expand Down
2 changes: 1 addition & 1 deletion decrypt/decrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func DataWithFormat(data []byte, format Format) (cleartext []byte, err error) {

// Data is a helper that takes encrypted data and a format string,
// decrypts the data and returns its cleartext in an []byte.
// The format string can be `json`, `yaml`, `ini`, `dotenv` or `binary`.
// The format string can be `json`, `yaml`, `ini`, `hcl`, `dotenv` or `binary`.
// If the format string is empty, binary format is assumed.
func Data(data []byte, format string) (cleartext []byte, err error) {
formatFmt := FormatFromString(format)
Expand Down
23 changes: 23 additions & 0 deletions functional-tests/res/format.enc.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
foo = "ENC[AES256_GCM,data:VWTe,iv:1jXojBlsfxaGel2OlCAY9i0CDaaVRCudGjuJlV90EA8=,tag:yzj4lEr4OaccODNka2shBg==,type:str]"

baz "qux" {
quux = [
"ENC[AES256_GCM,data:fw==,iv:Qykwql3Kahp2uoSgTLUv60HGniPU839v2i2zoDPuxJ4=,tag:ZYU3fHRnO+spNg0+qJAH/A==,type:int]",
"ENC[AES256_GCM,data:MA==,iv:CDH2zKXaYMhJr4c/528Z/hFM2IfLxlvQSc1cgVH+WT4=,tag:HDpCS1qqOJDU64PCpUcVZQ==,type:int]",
]
ref = "ENC[AES256_GCM,data:AVv1laLwAWU=,iv:E37lz0Sw+aFKC6sS5xtMELeu4a3DliYQR5z4Qs2Dox0=,tag:x/PC6Lfj7OpNdsployZLhg==,type:str]"
}

sops = {
lastmodified = "2026-10-16T20:25:39Z"
mac = "ENC[AES256_GCM,data:tg9KJb7A0nHIb+SmlZiry1DDQop8JIiJtN405BLNs1oBD1y3zb1XJjve5k837K6Hm7xTVrb4gw/MgW5c32IyZdjTqEQv+FusdYZ3ElK3OcIFFUoNvbakOBnGBTNA+h28ViH5c6LaH8awVftfdguOxouODqMhvlD5a0mHLy38oAc=,iv:lct8P+0EXKLNg6dh6elZk4kM09dzaljC1fbnCc3vv24=,tag:E8Wu1fqMUSPiuQG4Yg1a4Q==,type:str]"
pgp = [
{
created_at = "2026-10-16T20:25:39Z"
enc = "-----BEGIN PGP MESSAGE-----\n\nhQEMAyUpShfNkFB/AQf+MBQLTQghvVfi1n2Bu8F+0OP8WW7vx0ZKSUY19piKrh+G\nq8kfvpGkA+xHiQ3DGR034jeTwAcOBPnncJLF+omeeIUEO7frT9I4e7EMMBAKfubF\n5V/me5Oq85OhjvokzlxguTJeZvzgLll7KL4/vletIpUc2SHK9beK5dNiuVVSLjNc\nY4w9zM1t7SGQL/ueRmxCQBywAJsQbA8sclQHYI/0+C/fmjm1qEcYKdLwYDVCXSKB\nDOFhL4acuKu+eMljpxMe4vJ+dp+CQqF59WOKjYicDDSplYc2LVw9i7addD6NRRJJ\nQY7mfBFbATBfDyAAQdRINrbWpS05o/DdE41IQ/hl/tJeAbTDlkI0MI4U8p+LrERa\n6+VyeDX7tC/MNKzSznutBAbYnVRz4sZ4G5EBPJBVpSCcPEa8hmB+zcMaehjGwnFL\ndPXDtRMR4nUgzHi9Iha4OJFo5D+N5F2pFWFyqw1hyg==\n=0/eM\n-----END PGP MESSAGE-----"
fp = "FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4"
},
]
unencrypted_suffix = "_unencrypted"
version = "3.13.3"
}
18 changes: 18 additions & 0 deletions functional-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,24 @@ bar: |-
String::from_utf8_lossy(&output.stdout) == "[foo]\nbar = baz\n",
"Unexpected decrypted content"
);

// HCL
let output = Command::new(SOPS_BINARY_PATH)
.arg("decrypt")
.arg("res/format.enc.hcl")
.output()
.expect("Error running sops");
println!(
"stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(output.status.success(), "SOPS didn't return successfully");
assert!(
String::from_utf8_lossy(&output.stdout)
== "foo = \"bar\"\n\nbaz \"qux\" {\n quux = [\n 1,\n 2,\n ]\n ref = var.x\n}\n",
"Unexpected decrypted content"
);
}

#[test]
Expand Down
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/goware/prefixer v0.0.0-20160118172347-395022866408
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/hcl/v2 v2.25.0
github.com/hashicorp/vault/api v1.23.0
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.213
github.com/lib/pq v1.12.3
Expand All @@ -35,12 +36,14 @@ require (
github.com/sirupsen/logrus v1.10.2
github.com/stretchr/testify v1.12.1
github.com/urfave/cli v1.22.17
github.com/zclconf/go-cty v1.19.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/crypto v0.55.0
golang.org/x/net v0.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
google.golang.org/api v0.294.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688
google.golang.org/grpc v1.83.2
Expand Down Expand Up @@ -69,6 +72,9 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.58.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/apparentlymart/go-textseg/v17 v17.0.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 // indirect
Expand Down Expand Up @@ -148,9 +154,10 @@ require (
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto v0.0.0-20260720171339-e059f2f05d78 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260720171339-e059f2f05d78 // indirect
)
Loading