Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ OpenSVC v3 is a major evolution, rebuilt in Go for performance, reliability, and

* **Enhanced secret management**: New commands like `om <kvstore> key rename` for better key management in secret stores.

### Security

* **SSRF protection for HTTP fetches**:

All configuration, cfg, and secret
values fetched from HTTP(S) URLs are now validated against SSRF policies.
By default, only `https://raw.githubusercontent.com/opensvc/opensvc_templates/*`
URLs are allowed, while all other URLs and private/internal CIDR ranges
(including loopback, RFC 1918, link-local, TEST-NET, and IPv6 ULA) are blocked.
Redirects are disabled by default. Administrators can override these defaults
via environment variables in `/etc/default/opensvc` or `/etc/sysconfig/opensvc`:
```
OSVC_SSRF_ALLOWED_URL
OSVC_SSRF_BLOCKED_URL
OSVC_SSRF_ALLOWED_CIDR
OSVC_SSRF_BLOCKED_CIDR
OSVC_SSRF_ENABLE_REDIRECTS
```

### Network & Storage

* **Modern firewall management**: Configuration now uses nftables exclusively, with better support for large subnets including IPv6 via the new `mask_per_node` keyword.
Expand Down
2 changes: 1 addition & 1 deletion core/commoncmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func FlagCPUProfile(flags *pflag.FlagSet, p *string) {
}

func FlagCreateConfig(flags *pflag.FlagSet, p *string) {
flags.StringVar(p, "config", "", "the initial configuration source: -, /dev/stdin, file path, url, object path or template://<name>")
flags.StringVar(p, "config", "", "the initial configuration source: -, /dev/stdin, file path, http[s] url, object path or template://<name>")
}

func FlagCreateForce(flags *pflag.FlagSet, p *bool) {
Expand Down
2 changes: 1 addition & 1 deletion core/commoncmd/object_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func DataFromConfigURI(u uri.T) ([]byte, error) {
fpath, err := u.Fetch()
if err != nil {
return nil, nil
return nil, err
}
defer os.Remove(fpath)
return DataFromConfigFile(fpath)
Expand Down
4 changes: 4 additions & 0 deletions core/omcmd/cluster_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ func (t *CmdClusterJoin) extractCaClaim() (ca []byte, err error) {
token *jwt.Token
)

// Parse the token without verifying the signature (part of a secure
// bootstrap trust mechanism) to extract the CA certificate for a TLS trust
// establishment, not for authentication.
// Authentication happens server-side through JWT signature validation.
token, _, err = parser.ParseUnverified(t.Token, &joinClaim{})
if err != nil {
return
Expand Down
16 changes: 8 additions & 8 deletions core/omcmd/object_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func (t *CmdObjectCreate) do() error {
}
}

func (t CmdObjectCreate) fromPath(p naming.Path) error {
func (t *CmdObjectCreate) fromPath(p naming.Path) error {
cmd := CmdObjectConfigShow{}
b, err := cmd.extractPath(p, t.client)
if err != nil {
Expand All @@ -204,35 +204,35 @@ func (t CmdObjectCreate) fromPath(p naming.Path) error {
return t.fromData(p, b)
}

func (t CmdObjectCreate) fromTemplate(template string) error {
func (t *CmdObjectCreate) fromTemplate(template string) error {
if b, err := commoncmd.DataFromTemplate(template); err != nil {
return err
} else {
return t.fromData(t.path, b)
}
}

func (t CmdObjectCreate) fromConfig() error {
func (t *CmdObjectCreate) fromConfig() error {
b, err := t.dataFromConfig()
if err != nil {
return err
}
return t.fromData(t.path, b)
}

func (t CmdObjectCreate) fromScratch() error {
func (t *CmdObjectCreate) fromScratch() error {
return t.fromData(t.path, nil)
}

func (t CmdObjectCreate) fromStdin() error {
func (t *CmdObjectCreate) fromStdin() error {
b, err := commoncmd.DataFromStdin()
if err != nil {
return err
}
return t.fromData(t.path, b)
}

func (t CmdObjectCreate) dataFromConfig() ([]byte, error) {
func (t *CmdObjectCreate) dataFromConfig() ([]byte, error) {
u := uri.New(t.Config)
switch {
case file.Exists(t.Config):
Expand All @@ -244,7 +244,7 @@ func (t CmdObjectCreate) dataFromConfig() ([]byte, error) {
}
}

func (t CmdObjectCreate) fromData(p naming.Path, b []byte) error {
func (t *CmdObjectCreate) fromData(p naming.Path, b []byte) error {
if !t.Force && !t.Restore && p.Exists() {
return fmt.Errorf("%s already exists", p)
}
Expand Down Expand Up @@ -278,7 +278,7 @@ func (t CmdObjectCreate) fromData(p naming.Path, b []byte) error {
return nil
}

func (t CmdObjectCreate) localEmpty(p naming.Path) error {
func (t *CmdObjectCreate) localEmpty(p naming.Path) error {
if !t.Force && p.Exists() {
return fmt.Errorf("%s already exists", p)
}
Expand Down
14 changes: 7 additions & 7 deletions core/oxcmd/object_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (t *CmdObjectCreate) do() error {
}
}

func (t CmdObjectCreate) fromData(p naming.Path, b []byte) error {
func (t *CmdObjectCreate) fromData(p naming.Path, b []byte) error {
if !t.Force && p.Exists() {
return fmt.Errorf("%s already exists", p)
}
Expand Down Expand Up @@ -202,7 +202,7 @@ func (t CmdObjectCreate) fromData(p naming.Path, b []byte) error {
return nil
}

func (t CmdObjectCreate) fromPath(p naming.Path) error {
func (t *CmdObjectCreate) fromPath(p naming.Path) error {
cmd := CmdObjectConfigShow{}
b, err := cmd.extractFromDaemon(p, t.client)
if err != nil {
Expand All @@ -218,15 +218,15 @@ func (t CmdObjectCreate) fromPath(p naming.Path) error {
return t.fromData(p, b)
}

func (t CmdObjectCreate) fromTemplate(template string) error {
func (t *CmdObjectCreate) fromTemplate(template string) error {
if b, err := commoncmd.DataFromTemplate(template); err != nil {
return err
} else {
return t.fromData(t.path, b)
}
}

func (t CmdObjectCreate) fromConfig() error {
func (t *CmdObjectCreate) fromConfig() error {
b, err := t.dataFromConfig()
if err != nil {
return err
Expand All @@ -235,19 +235,19 @@ func (t CmdObjectCreate) fromConfig() error {
}
}

func (t CmdObjectCreate) fromScratch() error {
func (t *CmdObjectCreate) fromScratch() error {
return t.fromData(t.path, nil)
}

func (t CmdObjectCreate) fromStdin() error {
func (t *CmdObjectCreate) fromStdin() error {
b, err := commoncmd.DataFromStdin()
if err != nil {
return err
}
return t.fromData(t.path, b)
}

func (t CmdObjectCreate) dataFromConfig() ([]byte, error) {
func (t *CmdObjectCreate) dataFromConfig() ([]byte, error) {
u := uri.New(t.Config)
switch {
case file.Exists(t.Config):
Expand Down
10 changes: 10 additions & 0 deletions core/rawconfig/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ var (
Colorize *palette.ColorPaletteFunc
Color *palette.ColorPalette
Paths AgentPaths

SSRFAllowedURL []string
SSRFBlockedURL []string
SSRFAllowedCIDR []string
SSRFBlockedCIDR []string
SSRFEnableRedirects bool
)

func init() {
Expand All @@ -43,6 +49,10 @@ func Load(env map[string]string) {
}
}

if env != nil {
setSSRF(env)
}

var root string
if s, ok := os.LookupEnv("OSVC_ROOT_PATH"); ok {
root = s
Expand Down
62 changes: 62 additions & 0 deletions core/rawconfig/ssrf_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package rawconfig

import "strings"

var (
defaultSSRFAllowedURL = []string{
"https://raw.githubusercontent.com/opensvc/opensvc_templates/*",
}

defaultSSRFBlockedURL = []string{
"*",
}

defaultSSRFAllowedCIDR = []string{}

defaultSSRFBlockedCIDR = []string{
"127.0.0.0/8", // RFC 1122 loopback
"10.0.0.0/8", // RFC 1918 private
"172.16.0.0/12", // RFC 1918 private
"192.168.0.0/16", // RFC 1918 private
"169.254.0.0/16", // link local
"192.0.2.0/24", // RFC 5737 private TEST-NET-1
"198.51.100.0/24", // RFC 5737 private TEST-NET-2
"203.0.113.0/24", // RFC 5737 private TEST-NET-3

// IPV6
"::1/128", // RFC 4291 loopback
"fe80::/10", // link-local
"fc00::/7", // RFC 4193 Unique Local IPv6 Unicast Addresses
"ff00::/8", // reserved
}
)

// setSSRF configures SSRF protection settings, including allowed/blocked URLs, CIDR ranges, and redirect behavior.
// Default is:
// OSVC_SSRF_ALLOWED_URL = https://raw.githubusercontent.com/opensvc/opensvc_templates/*
// OSVC_SSRF_BLOCKED_URL = *
// OSVC_SSRF_BLOCKED_CIDR = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 192.0.2.0/24 198.51.100.0/24 203.0.113.0/24 ::1/128 fe80::/10 fc00::/7 ff00::/8
// OSVC_SSRF_ALLOWED_CIDR =
// OSVC_SSRF_ENABLE_REDIRECTS = false
func setSSRF(env map[string]string) {

SSRFAllowedURL = getSSRFValue(env, "OSVC_SSRF_ALLOWED_URL", defaultSSRFAllowedURL)
SSRFBlockedURL = getSSRFValue(env, "OSVC_SSRF_BLOCKED_URL", defaultSSRFBlockedURL)
SSRFAllowedCIDR = getSSRFValue(env, "OSVC_SSRF_ALLOWED_CIDR", defaultSSRFAllowedCIDR)
SSRFBlockedCIDR = getSSRFValue(env, "OSVC_SSRF_BLOCKED_CIDR", defaultSSRFBlockedCIDR)
if enableRedirects, _ := env["OSVC_SSRF_ENABLE_REDIRECTS"]; enableRedirects == "true" {
SSRFEnableRedirects = true
} else {
SSRFEnableRedirects = false
}
}

func getSSRFValue(env map[string]string, varName string, defaultValue []string) []string {
if v, ok := env[varName]; !ok {
return append([]string{}, defaultValue...)
} else if v == "" {
return []string{}
} else {
return append([]string{}, strings.Fields(v)...)
}
}
Loading
Loading