CVE-2025-54470
Description
This vulnerability affects NeuVector deployments only when the Report anonymous cluster data option is enabled. When this option is enabled, NeuVector sends anonymous telemetry data to the telemetry server.
In affected versions, NeuVector does not enforce TLS certificate verification when transmitting anonymous cluster data to the telemetry server. As a result, the communication channel is susceptible to man-in-the-middle (MITM) attacks, where an attacker could intercept or modify the transmitted data. Additionally, NeuVector loads the response of the telemetry server is loaded into memory without size limitation, which makes it vulnerable to a Denial of Service(DoS) attack
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/neuvector/neuvectorGo | >= 5.3.0, < 5.3.5 | 5.3.5 |
github.com/neuvector/neuvectorGo | >= 5.4.0, < 5.4.7 | 5.4.7 |
github.com/neuvector/neuvectorGo | >= 0.0.0-20230727023453-1c4957d53911, < 0.0.0-20251020133207-084a437033b4 | 0.0.0-20251020133207-084a437033b4 |
Affected products
1Patches
206424701e69bNVSHAS-9553-9979-9987
28 files changed · +1756 −350
controller/api/apis.go+8 −7 modified@@ -3865,13 +3865,14 @@ type RESTUserRoleConfigData struct { // Import task type RESTImportTask struct { - TID string `json:"tid"` - CtrlerID string `json:"ctrler_id"` - LastUpdateTime time.Time `json:"last_update_time,omitempty"` - Percentage int `json:"percentage"` - TriggeredBy string `json:"triggered_by,omitempty"` // fullname of the user who triggers import - Status string `json:"status,omitempty"` - TempToken string `json:"temp_token,omitempty"` + TID string `json:"tid"` + CtrlerID string `json:"ctrler_id"` + LastUpdateTime time.Time `json:"last_update_time,omitempty"` + Percentage int `json:"percentage"` + TriggeredBy string `json:"triggered_by,omitempty"` // fullname of the user who triggers import + Status string `json:"status,omitempty"` + TempToken string `json:"temp_token,omitempty"` + FailToDecryptKeyFields map[string][]string `json:"fail_to_decrypt_key_fields"` // key : []fields } type RESTImportTaskData struct {
controller/api/apis.yaml+8 −0 modified@@ -7519,6 +7519,14 @@ definitions: temp_token: type: string example: "" + fail_to_decrypt_key_fields: + type: object + description: Object key is kv key and value is array of cloaked fields that cannot be decrypted + additionalProperties: + type: array + items: + type: string + example: ["x509_cert", "signing_cert"] RESTImportTaskData: type: object required:
controller/api/internal_apis.go+1 −0 modified@@ -180,6 +180,7 @@ type AlertType string const ( AlertTypeRBAC AlertType = "RBAC" AlertTypeTlsCertificate AlertType = "TLS_CERTIFICATE" + AlertTypeOthers AlertType = "Others" ) type RESTNvAlertGroup struct {
controller/api/log_apis.go+4 −0 modified@@ -161,6 +161,10 @@ const ( EventNameGroupMetricViolation = "Group.Metric.Violation" EventNameKvRestored = "Configuration.Restore" EventNameScanDataRestored = "Scan.Data.Restore" + EventNameMismatchedDEKSeed = "Security.DEK.Seed.Mismatch" + EventNameDEKSeedUnavailable = "Security.DEK.Seed.Unavailable" + EventNameReEncryptWithDEK = "Security.DEK.Encrypt" + EventNameEncryptionSecretSet = "Security.Encryption.Secret.Set" ) // TODO: these are not events but incidents
controller/cache/cache.go+90 −5 modified@@ -1,12 +1,15 @@ package cache import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "net" "os" "reflect" "sort" + "strconv" "strings" "sync" "sync/atomic" @@ -178,11 +181,13 @@ const ( type Context struct { k8sVersion string ocVersion string - RancherEP string // from yaml/helm chart - RancherSSO bool // from yaml/helm chart - TelemetryFreq uint // from yaml - CheckDefAdminFreq uint // from yaml, in minutes - CspPauseInterval uint // from yaml, in minutes + RancherEP string // from yaml/helm chart + RancherSSO bool // from yaml/helm chart + TelemetryFreq uint // from yaml + KeyRotationDuration time.Duration // from yaml + CheckKeyRotationDuration time.Duration // from yaml + CheckDefAdminFreq uint // from yaml, in minutes + CspPauseInterval uint // from yaml, in minutes LocalDev *common.LocalDevice EvQueue cluster.ObjectQueueInterface AuditQueue cluster.ObjectQueueInterface @@ -1679,6 +1684,9 @@ func startWorkerThread(ctx *Context) { groupMetricCheckTicker := time.NewTicker(groupMetricCheckPeriod) policyMetricCheckTicker := time.NewTicker(policyMetricCheckPeriod) + keyRotationCheckTicker := time.NewTicker(cctx.CheckKeyRotationDuration) + log.WithFields(log.Fields{"CheckKeyRotationDuration": cctx.CheckKeyRotationDuration, "KeyRotationDuration": cctx.KeyRotationDuration}).Info() + wlSuspected := utils.NewSet() // supicious workload ids pruneKvTicker := time.NewTicker(pruneKVPeriod) pruneWorkloadKV(wlSuspected) // the first scan @@ -1742,6 +1750,8 @@ func startWorkerThread(ctx *Context) { cacheMutexUnlock() case <-pruneKvTicker.C: pruneWorkloadKV(wlSuspected) + case <-keyRotationCheckTicker.C: + checkKeyRotation() case <-scannerTicker.C: if isScanner() { // Remove stalled scanner @@ -2031,6 +2041,33 @@ func startWorkerThread(ctx *Context) { log.WithFields(log.Fields{"name": o.Name, "domain": o.Domain}).Info("deleted") } } + case resource.RscTypeSecret: + var err error + var lock cluster.LockInterface + for i := 0; i < 4; i++ { + var encKeys common.EncKeys + var currEncKeyVer string + lock, err = clusHelper.AcquireLock(share.CLUSStoreSecretKey, time.Duration(time.Second)*4) + if err == nil { + if encKeys, currEncKeyVer, _, err = resource.RetrieveStorePassphrases(); err == nil { + if len(encKeys) > 0 { + if err = common.InitAesGcmKey(encKeys, currEncKeyVer); err == nil { + log.Info("store passphrases reloaded") + clusHelper.ReleaseLock(lock) + break + } + log.WithFields(log.Fields{"i": i, "lead": isLeader(), "err": err}).Error("Failed to init AesGcm") + } + } else { + log.WithFields(log.Fields{"i": i, "err": err}).Error("Failed to reloaded store passphrases") + } + clusHelper.ReleaseLock(lock) + } + time.Sleep(time.Second) + } + if err != nil { + log.WithFields(log.Fields{"err": err}).Error("Failed to reloaded store passphrases") + } /* case resource.RscTypeMutatingWebhookConfiguration: var n, o *resource.AdmissionWebhookConfiguration @@ -2108,6 +2145,54 @@ func startWorkerThread(ctx *Context) { }() } +func checkKeyRotation() { + if !isLeader() { + return + } + + if valueBackup, _ := cluster.Get(share.CLUSSystemEncMigratedKey); len(valueBackup) > 0 { + var cfgEncBackup share.CLUSSystemConfigEncMigrated + if err := json.Unmarshal(valueBackup, &cfgEncBackup); err == nil { + if time.Now().UTC().After(cfgEncBackup.EncDataExpirationTime) { + // the backup is expired + _ = cluster.Delete(share.CLUSSystemEncMigratedKey) + log.Info("encryption-migrated config backup is deleted") + } + } + } + + value, _ := cluster.Get(share.CLUSNextKeyRotationTSKey) + if len(value) == 0 { + _ = kv.SetNextKeyRotationTime(cctx.KeyRotationDuration) + return + } + i, err := strconv.ParseInt(string(value), 10, 64) + if err != nil { + return + } + nextKeyRotationTime := time.Unix(i, 0) + if time.Now().After(nextKeyRotationTime) { + if err := resource.AddStorePassphrase(); err == nil { + if err = kv.SetNextKeyRotationTime(cctx.KeyRotationDuration); err == nil { + alert := "Important: Please backup the data in Kubernetes secret neuvector-store-secret reset by NeuVector" + b := sha256.Sum256([]byte(alert)) + key := hex.EncodeToString(b[:]) + allUser := clusHelper.GetAllUsers(access.NewReaderAccessControl()) + for _, user := range allUser { + accepted := utils.NewSetFromStringSlice(user.AcceptedAlerts) + if accepted.Contains(key) { + accepted.Remove(key) + user.AcceptedAlerts = accepted.ToStringSlice() + if err = clusHelper.PutUser(user); err != nil { + log.WithFields(log.Fields{"": user.Fullname, "err": err}).Error() + } + } + } + } + } + } +} + // handler of K8s resource watcher calls cbResourceWatcher() which sends to orchObjChan/objChan // [2021-02-15] CRD-related resource changes do not call this function. //
controller/cache/config.go+45 −0 modified@@ -32,6 +32,7 @@ type webhookCache struct { const DefaultScannerConfigUpdateTimeout = time.Minute * 5 +var encMigratedConfigRestored bool var systemConfigCache share.CLUSSystemConfig = common.DefaultSystemConfig var fedSystemConfigCache share.CLUSSystemConfig = share.CLUSSystemConfig{CfgType: share.FederalCfg} var webhookCacheMap map[string]*webhookCache = make(map[string]*webhookCache, 0) // Only the enabled webhooks @@ -467,6 +468,42 @@ func (m CacheMethod) GetSystemConfigClusterName(acc *access.AccessControl) strin return systemConfigCache.ClusterName } +func encMigrateSystemConfig(valueBackup, value []byte) { + controllers := cacher.GetAllControllers(access.NewAdminAccessControl()) + for _, ctrler := range controllers { + if ctrler.Ver != cctx.CtrlerVersion { + log.WithFields(log.Fields{"target": ctrler.Ver, "me": cctx.CtrlerVersion}).Info() + // the system config backup for variant encryption key migration is found. + // it means sensitive fields in system config are re-encrypted for variant encryption key migration. + var dec common.DecryptUnmarshaller + var cfg share.CLUSSystemConfig + if err := dec.Unmarshal(value, &cfg); err == nil && dec.GetDecryptedFieldsNumber() == 0 { + // however, it's unexpected that no sensitive field is decrypted in dec.Unmarshal() call. + // it means all sensitive fields in current system config(cfg) were set to empty string that we need to restore system config from the system config backup + var cfgEncBackup share.CLUSSystemConfigEncMigrated + if err = json.Unmarshal(valueBackup, &cfgEncBackup); err == nil { + if time.Now().UTC().Before(cfgEncBackup.EncDataExpirationTime) { + // the backup is not expired yet + if err = cluster.PutBinary(share.CLUSConfigSystemKey, []byte(cfgEncBackup.EncMigratedSystemConfig)); err == nil { + log.Info("encryption-migrated config is restored") + encMigratedConfigRestored = true + } + } else { + _ = cluster.Delete(share.CLUSSystemEncMigratedKey) + log.Info("encryption-migrated config backup is deleted") + } + } else { + _ = cluster.Delete(share.CLUSSystemEncMigratedKey) + log.WithFields(log.Fields{"err": err}).Error("failed to json unmarshal encryption-migrated config") + } + } else if err != nil { + log.WithFields(log.Fields{"err": err}).Error("failed to dec unmarshal system config") + } + break + } + } +} + func systemConfigUpdate(nType cluster.ClusterNotifyType, key string, value []byte) { log.WithFields(log.Fields{"type": cluster.ClusterNotifyName[nType], "key": key}).Debug() @@ -477,6 +514,14 @@ func systemConfigUpdate(nType cluster.ClusterNotifyType, key string, value []byt _ = json.Unmarshal(value, &cfg) log.WithFields(log.Fields{"config": cfg}).Debug() + if nType == cluster.ClusterNotifyModify { + if valueBackup, _ := cluster.Get(share.CLUSSystemEncMigratedKey); len(valueBackup) > 0 { + if !encMigratedConfigRestored { + encMigrateSystemConfig(valueBackup, value) + } + } + } + if cfg.IBMSAConfigNV.EpEnabled && cfg.IBMSAConfigNV.EpStart == 1 { if isLeader() { var param interface{} = &cfg.IBMSAConfig
controller/cache/object.go+1 −0 modified@@ -1646,6 +1646,7 @@ func ObjectUpdateHandler(nType cluster.ClusterNotifyType, key string, value []by case "uniconf": uniconfUpdate(nType, key, value) case "cert": + value, _, _ = kv.UpgradeAndConvert(key, value) certObjectUpdate(nType, key, value) case "throttled", "telemetry": default:
controller/cluster.go+4 −3 modified@@ -141,7 +141,7 @@ func leadChangeHandler(newLead, oldLead string) { } if emptyKvFound { - if _, _, restored, restoredKvVersion, err := kv.GetConfigHelper().Restore(); restored && err == nil { + if _, _, restored, restoredKvVersion, err := kv.GetConfigHelper().Restore(Host, Ctrler); restored && err == nil { clog := share.CLUSEventLog{ Event: share.CLUSEvKvRestored, HostID: Host.ID, @@ -216,7 +216,7 @@ func ctlrMemberUpdateHandler(nType cluster.ClusterNotifyType, memberAddr string, selfRejoin = false ctlrPutLocalInfo() - logController(share.CLUSEvControllerJoin) + logController(share.CLUSEvControllerJoin, "") } } else if nType == cluster.ClusterNotifyDelete { if Ctrler.ClusterIP == memberAddr { @@ -247,13 +247,14 @@ func clusterStart(clusterCfg *cluster.ClusterConfig) (string, string, error) { return cluster.GetSelfAddress(), lead, nil } -func logController(ev share.TLogEvent) { +func logController(ev share.TLogEvent, msg string) { clog := share.CLUSEventLog{ Event: ev, HostID: Host.ID, HostName: Host.Name, ControllerID: Ctrler.ID, ControllerName: Ctrler.Name, + Msg: msg, } switch ev { case share.CLUSEvControllerStart:
controller/common/common.go+7 −0 modified@@ -82,6 +82,9 @@ var ErrObjectExists error = errors.New("Object exists") var ErrAtomicWriteFail error = errors.New("Atomic write failed") var ErrUnsupported error = errors.New("Unsupported action") var ErrClusterWriteFail error = errors.New("Failed to write cluster") +var ErrInvalidPassphrase error = errors.New("Invalid passphrase for data encryption key") +var ErrDEKSeedUnavailable error = errors.New("store passphrase unavailable") +var ErrEmptyValue error = errors.New("Empty value") var defaultWebhookCategory []string = []string{} var defaultSyslogCategory []string = []string{ @@ -340,6 +343,10 @@ var LogEventMap = map[share.TLogEvent]LogEventInfo{ share.CLUSEvGroupMetricViolation: {api.EventNameGroupMetricViolation, api.EventCatGroup, api.LogLevelWARNING}, share.CLUSEvKvRestored: {api.EventNameKvRestored, api.EventCatConfig, api.LogLevelINFO}, share.CLUSEvScanDataRestored: {api.EventNameScanDataRestored, api.EventCatScan, api.LogLevelINFO}, + share.CLUSEvMismatchedDEKSeed: {api.EventNameMismatchedDEKSeed, api.EventCatConfig, api.LogLevelERR}, + share.CLUSEvDEKSeedUnavailable: {api.EventNameDEKSeedUnavailable, api.EventCatConfig, api.LogLevelWARNING}, + share.CLUSEvReEncryptWithDEK: {api.EventNameReEncryptWithDEK, api.EventCatConfig, api.LogLevelINFO}, + share.CLUSEvEncryptionSecretSet: {api.EventNameEncryptionSecretSet, api.EventCatConfig, api.LogLevelNOTICE}, } type LogIncidentInfo struct {
controller/common/marshal.go+409 −28 modified@@ -1,6 +1,8 @@ package common import ( + "crypto/aes" + "crypto/cipher" "crypto/pbkdf2" "crypto/rand" "crypto/sha256" @@ -9,18 +11,25 @@ import ( "encoding/json" "fmt" "reflect" + "strconv" "strings" + "sync" "github.com/neuvector/neuvector/controller/api" "github.com/neuvector/neuvector/share/utils" + log "github.com/sirupsen/logrus" ) type Marshaller interface { Marshal(data interface{}) ([]byte, error) + GetEmptyFieldsToEncrypt() utils.Set } type Unmarshaller interface { Unmarshal(raw []byte, data interface{}) error Uncloak(data interface{}) error + GetEmptyEncryptedFields() utils.Set + GetFailToDecryptFields() utils.Set + GetDecryptedFieldsNumber() int } const ( @@ -32,18 +41,197 @@ const ( ) const ( - saltSize = 16 + saltSize = 16 + nonceSize = 12 + + saltHexSize = 32 + nonceHexSize = 24 keyLength = 32 keyIter = 600000 - saltedHashPrefix = "s-" + DekSeedLength = 32 + + saltedHashPrefix = "s-" + cipherTypeA = "a" + cipherBundleFormat = "%s-%s-%s-%s-%s" // "{cipherTypeA}-{keyVersion}-{hex(salt)}-{hex(nonce)}-{hex(cipherText)}" + cipherBundleParts = 5 ) +type tMarshallResult struct { + emptyFieldsToEncrypt utils.Set // all to-cloak fields that have empty string value +} + +func (md *tMarshallResult) Reset() { + if md.emptyFieldsToEncrypt == nil { + md.emptyFieldsToEncrypt = utils.NewSet() + } else { + md.emptyFieldsToEncrypt.Clear() + } +} + +func (md *tMarshallResult) AddEmptyFieldToEncrypt(field string) { + if md.emptyFieldsToEncrypt != nil { + md.emptyFieldsToEncrypt.Add(field) + } +} + +func (md *tMarshallResult) GetEmptyFieldsToEncrypt() utils.Set { + if md.emptyFieldsToEncrypt != nil && md.emptyFieldsToEncrypt.Cardinality() > 0 { + return md.emptyFieldsToEncrypt.Clone() + } + return nil +} + +type tUnmarshallResult struct { + emptyEncryptedFields utils.Set // all cloaked fields that have empty string value + failedToDecryptFields utils.Set // all cloaked fields that cannot be decrypted + decryptedFieldsNumber int +} + +func (ud *tUnmarshallResult) Reset() { + if ud.emptyEncryptedFields == nil { + ud.emptyEncryptedFields = utils.NewSet() + } else { + ud.emptyEncryptedFields.Clear() + } + + if ud.failedToDecryptFields == nil { + ud.failedToDecryptFields = utils.NewSet() + } else { + ud.failedToDecryptFields.Clear() + } +} + +func (ud *tUnmarshallResult) AddEmptyEncryptedField(field string) { + if ud.emptyEncryptedFields != nil { + ud.emptyEncryptedFields.Add(field) + } +} + +func (ud *tUnmarshallResult) AddFailedToDecryptField(field string) { + if ud.failedToDecryptFields != nil { + ud.failedToDecryptFields.Add(field) + } +} + +func (ud *tUnmarshallResult) IncreaseDecryptedFields() { + ud.decryptedFieldsNumber++ +} + +func (ud *tUnmarshallResult) GetEmptyEncryptedFields() utils.Set { + if ud.emptyEncryptedFields != nil && ud.emptyEncryptedFields.Cardinality() > 0 { + return ud.emptyEncryptedFields.Clone() + } + return nil +} + +func (ud *tUnmarshallResult) GetFailToDecryptFields() utils.Set { + if ud.failedToDecryptFields != nil && ud.failedToDecryptFields.Cardinality() > 0 { + return ud.failedToDecryptFields.Clone() + } + return nil +} + +func (ud *tUnmarshallResult) GetDecryptedFieldsNumber() int { + return ud.decryptedFieldsNumber +} + type EmptyMarshaller struct{} type MaskMarshaller struct{} -type EncryptMarshaller struct{} -type DecryptUnmarshaller struct{} +type EncryptMarshaller struct { + result tMarshallResult +} +type DecryptUnmarshaller struct { + result tUnmarshallResult +} + +// specifically for import / restore purpose +type MigrateDecryptUnmarshaller struct { + ReEncryptRequired bool // set to true when any sensitive field needs to be re-encrypted by the new encryption mechanism + result tUnmarshallResult +} + +type EncKeys map[string][]byte // map key is enc key version(the bigger the newer). value is the passphrase +type nvDEK struct { + version string + dekSeed string +} + +func (m nvDEK) isAvailable() bool { + return len(m.dekSeed) == keyLength +} + +var dekSeedMutex sync.RWMutex +var currentDekSeed nvDEK // for the current dekSeed for encrypting data +var dekSeedsCache map[string]string = make(map[string]string) // hash values of all passphrases in k8s secret neuvector-store-secret + +func getCurrentDekSeed() nvDEK { + dekSeedMutex.RLock() + defer dekSeedMutex.RUnlock() + return currentDekSeed +} + +func getVersionedDekSeed(version string) string { + dekSeedMutex.RLock() + defer dekSeedMutex.RUnlock() + return dekSeedsCache[version] +} + +func InitAesGcmKey(encKeys EncKeys, currEncKeyVer string) error { + if len(encKeys) == 0 { + return ErrInvalidPassphrase + } + dekSeedsCacheTemp := make(map[string]string, len(encKeys)) + if passphrase, ok := encKeys[currEncKeyVer]; !ok || len(passphrase) < DekSeedLength { + return ErrInvalidPassphrase + } + + for keyVersion, passphrase := range encKeys { + if len(passphrase) >= DekSeedLength { + h := sha256.New() + h.Write(passphrase) + dekSeedsCacheTemp[keyVersion] = string(h.Sum(nil)) + } + } + + if len(dekSeedsCacheTemp[currEncKeyVer]) < DekSeedLength { + return ErrInvalidPassphrase + } + + dekSeedMutex.Lock() + dekSeedsCache = dekSeedsCacheTemp + currentDekSeed = nvDEK{ + version: currEncKeyVer, + dekSeed: dekSeedsCache[currEncKeyVer], + } + dekSeedMutex.Unlock() + + return nil +} + +func AddAesGcmKey(keyVersion string, passphrase []byte) error { + if len(passphrase) >= DekSeedLength { + h := sha256.New() + h.Write(passphrase) + dekSeed := string(h.Sum(nil)) + + dekSeedMutex.Lock() + dekSeedsCache[keyVersion] = dekSeed + currentDekSeed = nvDEK{ + version: keyVersion, + dekSeed: dekSeed, + } + dekSeedMutex.Unlock() + return nil + } + return ErrDEKSeedUnavailable +} + +func IsDEKSeedAvailable() bool { + dekSeed := getCurrentDekSeed() + return dekSeed.isAvailable() +} func HashPassword(password string, salt []byte) (string, error) { if len(salt) == 0 { @@ -64,6 +252,105 @@ func HashPassword(password string, salt []byte) (string, error) { return cipherBundle, nil } +func aesGcmEncrypt(plaintext string) (string, error) { + if plaintext == "" { + return "", ErrEmptyValue + } + dekSeed := getCurrentDekSeed() + if !dekSeed.isAvailable() { + return "", ErrDEKSeedUnavailable + } + + salt := make([]byte, saltSize) // http://www.ietf.org/rfc/rfc2898.txt + if _, err := rand.Read(salt); err != nil { + return "", fmt.Errorf("failed to generate salt: %w", err) + } + dek, err := pbkdf2.Key(sha256.New, currentDekSeed.dekSeed, salt, keyIter, keyLength) + if err != nil { + return "", fmt.Errorf("failed to derive DEK: %w", err) + } + + nonce := make([]byte, nonceSize) + if _, err := rand.Read(nonce); err != nil { + return "", fmt.Errorf("failed to generate nonce: %w", err) + } + + block, err := aes.NewCipher(dek) + if err != nil { + return "", fmt.Errorf("failed to create cipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM: %w", err) + } + + additionalData := append(salt, nonce...) + cipherText := aesGCM.Seal(nil, nonce, []byte(plaintext), additionalData) + cipherBundle := fmt.Sprintf(cipherBundleFormat, cipherTypeA, + currentDekSeed.version, hex.EncodeToString(salt), hex.EncodeToString(nonce), hex.EncodeToString(cipherText)) + + return cipherBundle, nil +} + +func aesGcmDecrypt(cipherBundle string) (string, error) { + if cipherBundle == "" { + return "", ErrEmptyValue + } + + ss := strings.Split(cipherBundle, "-") + if len(ss) != cipherBundleParts || ss[0] != cipherTypeA || (len(ss[2]) != saltHexSize || len(ss[3]) != nonceHexSize) { + return "", ErrUnsupported + } + + if _, err := strconv.ParseUint(ss[1], 10, 64); err != nil { + return "", fmt.Errorf("invalid version: %s", ss[1]) + } + + dekSeed := getVersionedDekSeed(ss[1]) + if len(dekSeed) != keyLength { + return "", ErrDEKSeedUnavailable + } + + salt, err := hex.DecodeString(ss[2]) + if err != nil || len(salt) != saltSize { + return "", fmt.Errorf("invalid salt: %v(len=%d)", err, len(salt)) + } + + nonce, err := hex.DecodeString(ss[3]) + if err != nil || len(nonce) != nonceSize { + return "", fmt.Errorf("invalid nonce: %v(len=%d)", err, len(nonce)) + } + + cipherText, err := hex.DecodeString(ss[4]) + if err != nil { + return "", fmt.Errorf("invalid data: %w", err) + } + + dek, err := pbkdf2.Key(sha256.New, dekSeed, salt, keyIter, keyLength) + if err != nil { + return "", fmt.Errorf("failed to derive DEK: %w", err) + } + + block, err := aes.NewCipher(dek) + if err != nil { + return "", fmt.Errorf("failed to create cipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM: %w", err) + } + + additionalData := append(salt, nonce...) + data, err := aesGCM.Open(nil, nonce, cipherText, additionalData) + if err != nil { + return "", fmt.Errorf("failed to decrypt data: %w", err) + } + + return string(data), nil +} + func IsSaltedPasswordHash(hash string) bool { if strings.HasPrefix(hash, saltedHashPrefix) { return len(strings.Split(hash, "-")) == 3 @@ -73,39 +360,88 @@ func IsSaltedPasswordHash(hash string) bool { } func (m EmptyMarshaller) Marshal(data interface{}) ([]byte, error) { - if u, err := marshal(emptyMask, data); err != nil { + if u, err := marshal(emptyMask, data, tMarshallResult{}); err != nil { return nil, err } else { return json.Marshal(u) } } +func (m EmptyMarshaller) GetEmptyFieldsToEncrypt() utils.Set { + return nil +} + func (m MaskMarshaller) Marshal(data interface{}) ([]byte, error) { - if u, err := marshal(cloakMask, data); err != nil { + if u, err := marshal(cloakMask, data, tMarshallResult{}); err != nil { return nil, err } else { return json.Marshal(u) } } -func (m EncryptMarshaller) Marshal(data interface{}) ([]byte, error) { - if u, err := marshal(cloakEncrypt, data); err != nil { +func (m *EncryptMarshaller) Marshal(data interface{}) ([]byte, error) { + m.result.Reset() + if u, err := marshal(cloakEncrypt, data, m.result); err != nil { return nil, err } else { return json.Marshal(u) } } -func (m DecryptUnmarshaller) Unmarshal(raw []byte, data interface{}) error { +func (m *EncryptMarshaller) GetEmptyFieldsToEncrypt() utils.Set { + return m.result.GetEmptyFieldsToEncrypt() +} + +func (m *DecryptUnmarshaller) Unmarshal(raw []byte, data interface{}) error { if err := json.Unmarshal(raw, data); err != nil { return err } else { - return unmarshal(cloakDecrypt, data) + m.result.Reset() + return unmarshal(cloakDecrypt, data, nil, m.result) } } -func (m DecryptUnmarshaller) Uncloak(data interface{}) error { - return unmarshal(cloakDecrypt, data) +func (m *DecryptUnmarshaller) Uncloak(data interface{}) error { + m.result.Reset() + return unmarshal(cloakDecrypt, data, nil, m.result) +} + +func (m *DecryptUnmarshaller) GetEmptyEncryptedFields() utils.Set { + return m.result.GetEmptyEncryptedFields() +} + +func (m *DecryptUnmarshaller) GetFailToDecryptFields() utils.Set { + return m.result.GetFailToDecryptFields() +} + +func (m *DecryptUnmarshaller) GetDecryptedFieldsNumber() int { + return m.result.decryptedFieldsNumber +} + +func (m *MigrateDecryptUnmarshaller) Unmarshal(raw []byte, data interface{}) error { + if err := json.Unmarshal(raw, data); err != nil { + return err + } else { + m.result.Reset() + return unmarshal(cloakDecrypt, data, &m.ReEncryptRequired, m.result) + } +} + +func (m *MigrateDecryptUnmarshaller) Uncloak(data interface{}) error { + m.result.Reset() + return unmarshal(cloakDecrypt, data, &m.ReEncryptRequired, m.result) +} + +func (m *MigrateDecryptUnmarshaller) GetEmptyEncryptedFields() utils.Set { + return m.result.GetEmptyEncryptedFields() +} + +func (m *MigrateDecryptUnmarshaller) GetFailToDecryptFields() utils.Set { + return m.result.GetFailToDecryptFields() +} + +func (m *MigrateDecryptUnmarshaller) GetDecryptedFieldsNumber() int { + return m.result.decryptedFieldsNumber } type MarshalInvalidTypeError struct { @@ -154,7 +490,7 @@ func parseTag(tag string) (string, utils.Set) { return tokens[0], utils.NewSetFromSliceKind(tokens[1:]) } -func unmarshal(cloak string, data interface{}) error { +func unmarshal(cloak string, data interface{}, reEncryptRequired *bool, unmarshalResult tUnmarshallResult) error { v := reflect.ValueOf(data) t := v.Type() @@ -168,7 +504,7 @@ func unmarshal(cloak string, data interface{}) error { } if t.Kind() != reflect.Struct { - return unmarshalValue(cloak, v) + return unmarshalValue(cloak, v, reEncryptRequired, unmarshalResult) } for i := 0; i < t.NumField(); i++ { @@ -200,15 +536,44 @@ func unmarshal(cloak string, data interface{}) error { switch cloak { case cloakDecrypt: if val.CanSet() { - s := utils.DecryptPassword(val.Interface().(string)) + var s string + var err error + if strVal := val.Interface().(string); strVal != "" { + if idx := strings.Index(strVal, "-"); idx < 0 { + // it's encrypted by the fixed default key. + if s = utils.DecryptPassword(strVal); s != "" { + if reEncryptRequired != nil { + *reEncryptRequired = true + } + unmarshalResult.IncreaseDecryptedFields() + } else { + s = strVal + } + } else { + // it's encrypted by variant DEK + if s, err = aesGcmDecrypt(strVal); err != nil { + if err != ErrDEKSeedUnavailable && err != ErrEmptyValue { + log.WithFields(log.Fields{"error": err, "jsonTag": jsonTag}).Error() + } + if err == ErrEmptyValue { + unmarshalResult.AddEmptyEncryptedField(jsonTag) + } else { + unmarshalResult.AddFailedToDecryptField(jsonTag) + } + s = strVal + } else { + unmarshalResult.IncreaseDecryptedFields() + } + } + } val.SetString(s) } } } } if val.CanAddr() { - err := unmarshalValue(cloak, val.Addr()) + err := unmarshalValue(cloak, val.Addr(), reEncryptRequired, unmarshalResult) if err != nil { return err } @@ -218,7 +583,7 @@ func unmarshal(cloak string, data interface{}) error { return nil } -func unmarshalValue(cloak string, v reflect.Value) error { +func unmarshalValue(cloak string, v reflect.Value, reEncryptRequired *bool, unmarshalResult tUnmarshallResult) error { // return nil on nil pointer struct fields if !v.IsValid() || !v.CanInterface() { return nil @@ -232,7 +597,7 @@ func unmarshalValue(cloak string, v reflect.Value) error { if k == reflect.Interface || k == reflect.Struct { if v.CanAddr() { - return unmarshal(cloak, v.Addr().Interface()) + return unmarshal(cloak, v.Addr().Interface(), reEncryptRequired, unmarshalResult) } else { return nil } @@ -241,7 +606,7 @@ func unmarshalValue(cloak string, v reflect.Value) error { if k == reflect.Slice { l := v.Len() for i := 0; i < l; i++ { - err := unmarshalValue(cloak, v.Index(i)) + err := unmarshalValue(cloak, v.Index(i), reEncryptRequired, unmarshalResult) if err != nil { return err } @@ -257,7 +622,7 @@ func unmarshalValue(cloak string, v reflect.Value) error { return MarshalInvalidTypeError{t: mapKeys[0].Kind(), data: v.Interface()} } for _, key := range mapKeys { - err := unmarshalValue(cloak, v.MapIndex(key)) + err := unmarshalValue(cloak, v.MapIndex(key), reEncryptRequired, unmarshalResult) if err != nil { return err } @@ -267,7 +632,7 @@ func unmarshalValue(cloak string, v reflect.Value) error { return nil } -func marshal(cloak string, data interface{}) (interface{}, error) { +func marshal(cloak string, data interface{}, marshalResult tMarshallResult) (interface{}, error) { v := reflect.ValueOf(data) t := v.Type() @@ -281,7 +646,7 @@ func marshal(cloak string, data interface{}) (interface{}, error) { } if t.Kind() != reflect.Struct { - return marshalValue(cloak, v) + return marshalValue(cloak, v, marshalResult) } dest := make(map[string]interface{}) @@ -326,13 +691,29 @@ func marshal(cloak string, data interface{}) (interface{}, error) { m := api.RESTMaskedValue val = reflect.ValueOf(m) case cloakEncrypt: - m := utils.EncryptPassword(val.Interface().(string)) + var m string + if strVal := val.Interface().(string); strVal != "" { + if currentDekSeed.isAvailable() { + var err error + if m, err = aesGcmEncrypt(strVal); err != nil { + if err != ErrDEKSeedUnavailable && err != ErrEmptyValue { + log.WithFields(log.Fields{"err": err, "jsonTag": jsonTag}).Error() + } + if err == ErrEmptyValue { + marshalResult.AddEmptyFieldToEncrypt(jsonTag) + } + m = utils.EncryptPassword(strVal) + } + } else { + m = utils.EncryptPassword(strVal) + } + } val = reflect.ValueOf(m) } } } - v, err := marshalValue(cloak, val) + v, err := marshalValue(cloak, val, marshalResult) if err != nil { return nil, err } @@ -350,7 +731,7 @@ func marshal(cloak string, data interface{}) (interface{}, error) { return dest, nil } -func marshalValue(cloak string, v reflect.Value) (interface{}, error) { +func marshalValue(cloak string, v reflect.Value, marshalResult tMarshallResult) (interface{}, error) { // return nil on nil pointer struct fields if !v.IsValid() || !v.CanInterface() { return nil, nil @@ -374,7 +755,7 @@ func marshalValue(cloak string, v reflect.Value) (interface{}, error) { } if k == reflect.Interface || k == reflect.Struct { - return marshal(cloak, val) + return marshal(cloak, val, marshalResult) } if k == reflect.Slice { if isEmptyValue(v) { @@ -384,7 +765,7 @@ func marshalValue(cloak string, v reflect.Value) (interface{}, error) { l := v.Len() dest := make([]interface{}, l) for i := 0; i < l; i++ { - d, err := marshalValue(cloak, v.Index(i)) + d, err := marshalValue(cloak, v.Index(i), marshalResult) if err != nil { return nil, err } @@ -411,7 +792,7 @@ func marshalValue(cloak string, v reflect.Value) (interface{}, error) { return nil, MarshalInvalidTypeError{t: mapKeys[0].Kind(), data: val} } for _, key := range mapKeys { - d, err := marshalValue(cloak, v.MapIndex(key)) + d, err := marshalValue(cloak, v.MapIndex(key), marshalResult) if err != nil { return nil, err }
controller/common/marshal_test.go+317 −0 modified@@ -1,6 +1,7 @@ package common import ( + "strings" "testing" "encoding/json" @@ -123,6 +124,322 @@ func TestEncrypt(t *testing.T) { } } +func resetDekSeeds() { + dekSeedMutex.Lock() + defer dekSeedMutex.Unlock() + + currentDekSeed = nvDEK{} + dekSeedsCache = make(map[string]string) +} + +// just for unit test +func _deleteAesGcmKey(keyVersion string) { + dekSeedMutex.Lock() + defer dekSeedMutex.Unlock() + + delete(dekSeedsCache, keyVersion) + if currentDekSeed.version == keyVersion { + if len(dekSeedsCache) > 0 { + for k, v := range dekSeedsCache { + currentDekSeed = nvDEK{ + version: k, + dekSeed: v, + } + break + } + } else { + currentDekSeed = nvDEK{} + } + } +} + +func TestAesGcmEncrypt(t *testing.T) { + var enc EncryptMarshaller + var dec DecryptUnmarshaller + + secret := "gary321" + u1 := maskUser{Username: "gary", Password: "gary123", Secret: &secret} + u2 := maskUser{Username: "mary", Password: "mary123", Secret: &secret} + + var user maskUser + keyVersion := "1" + if err := InitAesGcmKey(map[string][]byte{keyVersion: []byte("abcdefghijklmnopqrstuvwxyz123456")}, keyVersion); err != nil { + t.Errorf("InitAesGcmKey failed: error=%v", err) + } + + body, err := enc.Marshal(&u1) // enc marshal with DEK + if u1.Secret == nil || err != nil { + t.Errorf("enc.Marshal error: %v (u1.Secret=%v)", err, u1.Secret) + return + } + + // now sensitive fields in 'body' is encrypted with DEK. + + if err := json.Unmarshal(body, &user); err != nil { // sensitive fields(encrypted) are not decrypted after json unmarshal + t.Errorf("json.Unmarshal error: %v", err) + } else { + if ss := strings.Split(*user.Secret, "-"); len(ss) != cipherBundleParts { + t.Errorf("Unexpected json.Unmarshal result: %v (user.Secret=%v)", err, *user.Secret) + } + } + + if err := dec.Unmarshal(body, &user); err != nil { // dec unmarshal with DEK + t.Errorf("dec.Unmarshal error: %v", err) + } else if failed := dec.GetFailToDecryptFields(); failed != nil && failed.Cardinality() > 0 { + t.Errorf("Failed to decrypt %v", failed) + } else if !reflect.DeepEqual(user, u1) { + t.Errorf("Incorrect mask marshal: marshal=%s, user=%v, u1=%v", string(body[:]), user, u1) + body, _ := json.Marshal(&user) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + + var pair maskUserPair + p := maskUserPair{User1: u1, User2: &u2} + body, err = enc.Marshal(&p) // enc marshal with DEK + if p.User1.Secret == nil || p.User2.Secret == nil || err != nil { + t.Errorf("enc.Marshal error: %v (p.User1.Secret=%v, p.User2.Secret=%v)", err, p.User1.Secret, p.User2.Secret) + } else { + if err := json.Unmarshal(body, &pair); err != nil { // sensitive fields(encrypted) are not decrypted after json unmarshal + t.Errorf("json.Unmarshal error: %v", err) + } else { + if ss := strings.Split(*pair.User1.Secret, "-"); len(ss) != cipherBundleParts { + t.Errorf("Unexpected pair.User1 Marshal result: %v (pair.User1.Secret=%v)", err, *pair.User1.Secret) + } else if ss := strings.Split(*pair.User2.Secret, "-"); len(ss) != cipherBundleParts { + t.Errorf("Unexpected pair.User2 Marshal result: %v (pair.User2.Secret=%v)", err, *pair.User2.Secret) + } + } + + if err := dec.Unmarshal(body, &pair); err != nil { // dec unmarshal with DEK + t.Errorf("dec.Unmarshal error: %v", err) + } else if failed := dec.GetFailToDecryptFields(); failed != nil && failed.Cardinality() > 0 { + t.Errorf("Failed to decrypt %v", failed) + } else if !reflect.DeepEqual(pair.User1, u1) || !reflect.DeepEqual(*pair.User2, u2) { + t.Errorf("Incorrect mask marshal: marshal=%s", string(body[:])) + body, _ := json.Marshal(&pair) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + } + + resetDekSeeds() +} + +func TestAesGcmDecryptWithRotation(t *testing.T) { + var enc EncryptMarshaller + var dec DecryptUnmarshaller + + secret := "gary321" + u1 := maskUser{Username: "gary", Password: "gary123", Secret: &secret} + + var user maskUser + keyVersion1 := "1" + if err := InitAesGcmKey(map[string][]byte{keyVersion1: []byte("11cdefghijklmnopqrstuvwxyz123456")}, keyVersion1); err != nil { + t.Errorf("InitAesGcmKey failed: error=%v", err) + } + currDekSeed1 := getCurrentDekSeed() + if currDekSeed1.version != keyVersion1 || !currDekSeed1.isAvailable() { + t.Errorf("currDekSeed1 error: %v", currDekSeed1) + } + + body, err := enc.Marshal(&u1) // enc marshal with DEK (current DEK is dekSeed-v1) + if u1.Secret == nil || err != nil { + t.Errorf("enc.Marshal error: %v (u1.Secret=%v)", err, u1.Secret) + return + } + + // now sensitive fields in 'body' is encrypted with dekSeed-v1. + + if err := json.Unmarshal(body, &user); err != nil { // sensitive fields(encrypted) are not decrypted after json unmarshal + t.Errorf("json.Unmarshal error: %v", err) + } else { + if ss := strings.Split(*user.Secret, "-"); len(ss) != cipherBundleParts { + t.Errorf("Unexpected json.Unmarshal result: %v (user.Secret=%v)", err, *user.Secret) + } + } + + if err := dec.Unmarshal(body, &user); err != nil { // dec unmarshal (current DEK is dekSeed-v1) + t.Errorf("dec.Unmarshal error: %v", err) + } else if failed := dec.GetFailToDecryptFields(); failed != nil && failed.Cardinality() > 0 { + t.Errorf("Failed to decrypt %v", failed) + } else if !reflect.DeepEqual(user, u1) { + t.Errorf("Incorrect mask marshal: marshal=%s, user=%v, u1=%v", string(body[:]), user, u1) + body, _ := json.Marshal(&user) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + + // now currentDekSeed is set to version 2 + keyVersion2 := "2" + if err := AddAesGcmKey(keyVersion2, []byte("22cdefghijklmnopqrstuvwxyz123456")); err != nil { + t.Errorf("AddAesGcmKey failed: error=%v", err) + } + currDekSeed2 := getCurrentDekSeed() + if currDekSeed2 == currDekSeed1 || currDekSeed2.version != keyVersion2 || !currDekSeed2.isAvailable() { + t.Errorf("currDekSeed2 error: %v", currDekSeed2) + } + + // try dec.Unmarshal again to see whether the sensitive data encrypted with dekSeed-v1 can be decrypted when the currentDekSeed is version 2. + user = maskUser{} + if err := dec.Unmarshal(body, &user); err != nil { // dec unmarshal (current DEK is dekSeed-v2 but dekSeed-v1 is used for this decryption) + t.Errorf("dec.Unmarshal error: %v", err) + } else if failed := dec.GetFailToDecryptFields(); failed != nil && failed.Cardinality() > 0 { + t.Errorf("Failed to decrypt %v", failed) + } else if !reflect.DeepEqual(user, u1) { + t.Errorf("Incorrect mask marshal: marshal=%s, user=%v, u1=%v", string(body[:]), user, u1) + body, _ := json.Marshal(&user) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + + // now intentionally delete dekSeed-v1 from cache (ideally should not happen in real world) + _deleteAesGcmKey(keyVersion1) + + // try dec.Unmarshal again. currentDekSeed is version 2 & there is no dekSeed-v1 in the cache. + // it's expected that the sensitive data encrypted by dekSeed-v1 can not be decrypted anymore. + user = maskUser{} + if err := dec.Unmarshal(body, &user); err != nil { // dec unmarshal (current DEK is dekSeed-v2 and there is no dekSeed-v1 in the cache) + t.Errorf("dec.Unmarshal error: %v", err) + } else if failed := dec.GetFailToDecryptFields(); failed == nil || failed.Cardinality() == 0 { + t.Errorf("Expected to fail to decrypt sensitive fields in the object") + } else if failed.Cardinality() != 2 { + t.Errorf("Expected to fail to decrypt 2, not %d, sensitive fields in the object", failed.Cardinality()) + } else if !failed.Contains("password") || !failed.Contains("secret") { + t.Errorf("Expected to fail to decrypt sensitive fields, %v, in the object", failed.ToStringSlice()) + } + + // Now try enc.Marshal again. Because current DEK is dekSeed-v2, it's expected the marshaled data is different + body2, err2 := enc.Marshal(&u1) // enc marshal with DEK (current DEK is dekSeed-v2) + if u1.Secret == nil || err2 != nil { + t.Errorf("enc.Marshal error: %v (u1.Secret=%v)", err, u1.Secret) + } else if string(body) == string(body2) { + t.Errorf("Expected to have different marshaled data because sensitive fields in the objects are encrypted with different dekSeed") + } + + resetDekSeeds() +} + +func TestAesGcmDecryptNegative(t *testing.T) { + var enc EncryptMarshaller + var dec DecryptUnmarshaller + + secret := "gary321" + u1 := maskUser{Username: "gary", Password: "gary123", Secret: &secret} + + var user maskUser + keyVersion := "1" + if err := InitAesGcmKey(map[string][]byte{keyVersion: []byte("abcdefghijklmnopqrstuvwxyz123456")}, keyVersion); err != nil { + t.Errorf("[1] InitAesGcmKey failed: error=%v", err) + } + + body, err := enc.Marshal(&u1) // enc marshal with DEK + if u1.Secret == nil || err != nil { + t.Errorf("enc.Marshal error: %v (u1.Secret=%v)", err, u1.Secret) + return + } + + // now sensitive fields in 'body' is encrypted with DEK. + + // change dekSeed to different value + if err := InitAesGcmKey(map[string][]byte{keyVersion: []byte("abcdefghijklmnopqrstuvwxyz123457")}, keyVersion); err != nil { + t.Errorf("[2] InitAesGcmKey failed: error=%v", err) + } + + // try to decrypt with different dekSeed + if err := dec.Unmarshal(body, &user); err != nil { // dec unmarshal with DEK + t.Errorf("dec.Unmarshal error: %v", err) + } else if failed := dec.GetFailToDecryptFields(); failed.Cardinality() == 0 { + t.Errorf("Unexpected that all sensitive fields can be decrypt") + } else if reflect.DeepEqual(user, u1) { + t.Errorf("Incorrect mask marshal: marshal=%s", string(body[:])) + body, _ := json.Marshal(&user) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + + resetDekSeeds() +} + +func TestAesGcmMigrateDecryptUnmarshaller(t *testing.T) { + var enc EncryptMarshaller + var dec DecryptUnmarshaller + + secret := "gary321" + u1 := maskUser{Username: "gary", Password: "gary123", Secret: &secret} + + resetDekSeeds() + + var user maskUser + body, err := enc.Marshal(&u1) // enc marshal with fixed default key + if u1.Secret == nil || err != nil { + t.Errorf("enc.Marshal error: %v (u1.Secret=%v)", err, u1.Secret) + return + } + + // now sensitive fields in 'body' is encrypted with fixed default key. + + if err := json.Unmarshal(body, &user); err != nil { // sensitive fields(encrypted) are not decrypted after json unmarshal + t.Errorf("json.Unmarshal error: %v", err) + } else if ss := strings.Split(*user.Secret, "-"); len(ss) != 1 { + t.Errorf("Unexpected json.Marshal result: %v (user.Secret=%v)", err, *user.Secret) + } + + if err := dec.Unmarshal(body, &user); err != nil { // dec unmarshal with fixed default key + t.Errorf("dec.Unmarshal error: %v", err) + } else if !reflect.DeepEqual(user, u1) { + t.Errorf("Incorrect mask marshal: marshal=%s", string(body[:])) + body, _ := json.Marshal(&user) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + + keyVersion := "1" + if err := InitAesGcmKey(map[string][]byte{keyVersion: []byte("abcdefghijklmnopqrstuvwxyz123456")}, keyVersion); err != nil { + t.Errorf("InitAesGcmKey failed: error=%v", err) + } + + // now DEK & fixed default key are available + + var user1 maskUser + var dec2 MigrateDecryptUnmarshaller + if err := dec2.Unmarshal(body, &user1); err != nil { // dec unmarshal with fixed default key & set dec2.ReEncryptRequired to true + t.Errorf("Unmarshal error: %v", err) + } else if !dec2.ReEncryptRequired { + t.Errorf("Expect dec2.ReEncryptRequired=true but not see that") + } else { + if !reflect.DeepEqual(user1, u1) { + t.Errorf("Incorrect mask marshal: marshal=%s", string(body[:])) + body, _ := json.Marshal(&user) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + } + + var user2 maskUser + if err := dec.Unmarshal(body, &user2); err != nil { // dec unmarshal with fixed default key + t.Errorf("dec.Unmarshal error: %v", err) + } else { + if !reflect.DeepEqual(user1, user2) { + t.Errorf("Incorrect mask marshal: marshal=%s", string(body[:])) + body, _ := json.Marshal(&user2) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + } + + var user3 maskUser + if err := json.Unmarshal(body, &user3); err != nil { // sensitive fields(encrypted) are not decrypted after json unmarshal + t.Errorf("json.Unmarshal error: %v", err) + } else { + var dec3 MigrateDecryptUnmarshaller + if err := dec3.Uncloak(&user3); err != nil { // uncloak sensitive fields with fixed default key & set dec3.ReEncryptRequired to true + t.Errorf("dec3.Uncloak error: %v", err) + } else if !dec3.ReEncryptRequired { + t.Errorf("Expect dec3.ReEncryptRequired=true but not see that") + } else { + if !reflect.DeepEqual(user3, u1) { + t.Errorf("Incorrect mask marshal: marshal=%s", string(body[:])) + body, _ := json.Marshal(&user3) + t.Errorf("Incorrect mask marshal: unmarshal=%s", string(body[:])) + } + } + } + + resetDekSeeds() +} + type aType struct { IP net.IP `json:"ip"` Array []byte `json:"array"`
controller/controller.go+126 −36 modified@@ -263,9 +263,12 @@ func main() { pwdValidUnit := flag.Uint("pwd_valid_unit", 1440, "") rancherEP := flag.String("rancher_ep", "", "Rancher endpoint URL") rancherSSO := flag.Bool("rancher_sso", false, "Rancher SSO integration") + insecureSkipTeleTLSVerification := flag.Bool("insecure_skip_telemetry_tls_verification", false, "Do not verify Telemetry server's certificate chain and host name") teleNeuvectorEP := flag.String("telemetry_neuvector_ep", "", "") // for testing only teleCurrentVer := flag.String("telemetry_current_ver", "", "") // in the format {major}.{minor}.{patch}[-s{#}], for testing only telemetryFreq := flag.Uint("telemetry_freq", 60, "") // in minutes, for testing only + keyRotationPeriod := flag.String("key_rotation_period", "", "") // for testing only + checkKeyRotationPeriod := flag.String("check_key_rotation_period", "", "") // for testing only noDefAdmin := flag.Bool("no_def_admin", false, "Do not create default admin user") // for new install only cspEnv := flag.String("csp_env", "", "") // "" or "aws" cspPauseInterval := flag.Uint("csp_pause_interval", 240, "") // in minutes, for testing only @@ -484,6 +487,34 @@ func main() { if platform == share.PlatformKubernetes { resource.AdjustAdmWebhookName(cache.QueryK8sVersion, cspType) + + resource.GetNvCtrlerServiceAccount() + if !nvHasK8sSecretRbac(true) { + goodRBAC := false + ticker := time.Tick(time.Second * time.Duration(5)) + c_sig := make(chan os.Signal, 1) + signal.Notify(c_sig, os.Interrupt, syscall.SIGTERM) + exit_controller: + for { + select { + case <-ticker: + logLevel := log.GetLevel() + log.SetLevel(log.WarnLevel) + goodRBAC = nvHasK8sSecretRbac(false) + log.SetLevel(logLevel) + if goodRBAC { + log.Info("Required Kubernetes RBAC for secrets are found") + break exit_controller + } + case <-c_sig: + break exit_controller + } + } + if !goodRBAC { + log.Error("Required Kubernetes RBAC for secrets are not found") + os.Exit(-2) + } + } } // Assign controller interface/IP scope @@ -582,7 +613,21 @@ func main() { log.WithFields(log.Fields{"error": err}).Error("CreateVulAssetDb") } - kv.Init(Ctrler.ID, dev.Ctrler.Ver, Host.Platform, Host.Flavor, *persistConfig, isGroupMember, getConfigKvData, evqueue) + keyRotationDuration := time.Duration(time.Hour * 24 * 30 * 3) + checkKeyRotationDuration := time.Duration(time.Hour) + if *keyRotationPeriod != "" { + if duration, err := time.ParseDuration(*keyRotationPeriod); err == nil && duration.Seconds() != 0 { + keyRotationDuration = duration + } + } + if *checkKeyRotationPeriod != "" { + if duration, err := time.ParseDuration(*checkKeyRotationPeriod); err == nil && duration.Seconds() != 0 { + checkKeyRotationDuration = duration + } + } + + kv.Init(Ctrler.ID, dev.Ctrler.Ver, Host.Platform, Host.Flavor, *persistConfig, isGroupMember, + getConfigKvData, evqueue, keyRotationDuration) ruleid.Init() // Start cluster @@ -664,13 +709,52 @@ func main() { log.WithError(err).Warn("installation id is not readable. Will retry later.") } + var dekSeedEvent share.TLogEvent = share.CLUSEvDEKSeedUnavailable + var dekSeedEventMsg string + if platform == share.PlatformKubernetes { + var err error + var lock cluster.LockInterface + var encKeys common.EncKeys + var currEncKeyVer string + dekSeedEventMsg = "Failed to generate store passphrase for DEK seed" + for i := 0; i < 4; i++ { + lock, err = clusHelper.AcquireLock(share.CLUSStoreSecretKey, time.Duration(time.Second)*4) + if err == nil { + var msg string + if encKeys, currEncKeyVer, msg, err = resource.RetrieveStorePassphrases(); err == nil { + if err = common.InitAesGcmKey(encKeys, currEncKeyVer); err == nil { + if msg != "" { + dekSeedEvent = share.CLUSEvEncryptionSecretSet + dekSeedEventMsg = msg + log.Info(msg) + } + log.Info("store passphrase generate/read") + } else { + log.WithFields(log.Fields{"err": err, "len": len(encKeys)}).Error("Failed to init AesGcm") + } + } else { + log.WithFields(log.Fields{"err": err}).Error("Failed to generate/read store passphrase") + } + clusHelper.ReleaseLock(lock) + break + } + log.WithFields(log.Fields{"i": i, "err": err}).Info("retry for store passphrase") + time.Sleep(time.Second) + } + if err != nil || len(encKeys) == 0 { + log.WithFields(log.Fields{"err": err}).Error("Failed to read store passphrases") + os.Exit(-2) + } + } + emptyKvFound := false ver := kv.GetControlVersion() if ver.CtrlVersion == "" && ver.KVVersion == "" { emptyKvFound = true } log.WithFields(log.Fields{"emptyKvFound": emptyKvFound, "ver": ver}).Info() + var restoreEvent string if Ctrler.Leader || emptyKvFound { // See [NVSHAS-5490]: // clusterHelper.AcquireLock() may fail with error "failed to create session: Unexpected response code: 500 (Missing node registration)". @@ -694,20 +778,10 @@ func main() { var restored bool var restoredKvVersion string var errRestore error - restoredFedRole, defAdminRestored, restored, restoredKvVersion, errRestore = kv.GetConfigHelper().Restore() + restoredFedRole, defAdminRestored, restored, restoredKvVersion, errRestore = kv.GetConfigHelper().Restore(Host, Ctrler) + log.WithFields(log.Fields{"restored": restored, "errRestore": errRestore}).Info() if restored && errRestore == nil { - clog := share.CLUSEventLog{ - Event: share.CLUSEvKvRestored, - HostID: Host.ID, - HostName: Host.Name, - ControllerID: Ctrler.ID, - ControllerName: Ctrler.Name, - ReportedAt: time.Now().UTC(), - Msg: fmt.Sprintf("Restored kv version: %s", restoredKvVersion), - } - if err := evqueue.Append(&clog); err != nil { - log.WithFields(log.Fields{"error": err}).Error("evqueue.Append") - } + restoreEvent = fmt.Sprintf("Restored kv version: %s", restoredKvVersion) } if restoredFedRole == api.FedRoleJoint { // fed rules are not restored on joint cluster but there might be fed rules left in kv so @@ -838,6 +912,8 @@ func main() { RancherEP: *rancherEP, RancherSSO: *rancherSSO, TelemetryFreq: *telemetryFreq, + KeyRotationDuration: keyRotationDuration, + CheckKeyRotationDuration: checkKeyRotationDuration, CheckDefAdminFreq: checkDefAdminFreq, LocalDev: dev, EvQueue: evqueue, @@ -885,7 +961,7 @@ func main() { if platform == share.PlatformKubernetes { // k8s rbac watcher won't know anything about non-existing resources - resource.GetNvCtrlerServiceAccount(cache.CacheEvent) + resource.SetCacheEventFunc(cache.CacheEvent) clusterRoleErrors, clusterRoleBindingErrors, roleErrors, roleBindingErrors := resource.VerifyNvK8sRBAC(dev.Host.Flavor, "", true) if len(clusterRoleErrors) > 0 || len(roleErrors) > 0 || len(clusterRoleBindingErrors) > 0 || len(roleBindingErrors) > 0 { @@ -903,24 +979,25 @@ func main() { opa.InitOpaServer() rctx := rest.Context{ - LocalDev: dev, - EvQueue: evqueue, - AuditQueue: auditQueue, - Messenger: messenger, - Cacher: cacher, - Scanner: scanner, - RESTPort: *restPort, - FedPort: *fedPort, - PwdValidUnit: *pwdValidUnit, - TeleNeuvectorURL: *teleNeuvectorEP, - SearchRegistries: *searchRegistries, - TeleFreq: *telemetryFreq, - NvAppFullVersion: nvAppFullVersion, - NvSemanticVersion: nvSemanticVersion, - CspType: cspType, - CspPauseInterval: *cspPauseInterval, - CustomCheckControl: *custom_check_control, - CheckCrdSchemaFunc: nvcrd.CheckCrdSchema, + LocalDev: dev, + EvQueue: evqueue, + AuditQueue: auditQueue, + Messenger: messenger, + Cacher: cacher, + Scanner: scanner, + RESTPort: *restPort, + FedPort: *fedPort, + PwdValidUnit: *pwdValidUnit, + TeleNeuvectorURL: *teleNeuvectorEP, + TeleSkipTlsVerification: *insecureSkipTeleTLSVerification, + SearchRegistries: *searchRegistries, + TeleFreq: *telemetryFreq, + NvAppFullVersion: nvAppFullVersion, + NvSemanticVersion: nvSemanticVersion, + CspType: cspType, + CspPauseInterval: *cspPauseInterval, + CustomCheckControl: *custom_check_control, + CheckCrdSchemaFunc: nvcrd.CheckCrdSchema, } // rest.PreInitContext() must be called before orch connector because existing CRD handling could happen right after orch connecter starts rest.PreInitContext(&rctx) @@ -1033,8 +1110,14 @@ func main() { c_sig := make(chan os.Signal, 1) signal.Notify(c_sig, os.Interrupt, syscall.SIGTERM) - logController(share.CLUSEvControllerStart) - logController(share.CLUSEvControllerJoin) + if dekSeedEventMsg != "" { + logController(dekSeedEvent, dekSeedEventMsg) + } + if restoreEvent != "" { + logController(share.CLUSEvKvRestored, restoreEvent) + } + logController(share.CLUSEvControllerStart, "") + logController(share.CLUSEvControllerJoin, "") cache.PopulateRulesToOpa() @@ -1088,7 +1171,7 @@ func main() { memorySnapshot(mStats.WorkingSet) } case <-c_sig: - logController(share.CLUSEvControllerStop) + logController(share.CLUSEvControllerStop, "") flushEventQueue() done <- true } @@ -1171,3 +1254,10 @@ func amendNotPrivilegedMode() error { } return nil } + +func nvHasK8sSecretRbac(logging bool) bool { + k8sRbacRequried := []string{resource.NvSecretRole, resource.NvSecretControllerRole} + errs, _ := resource.VerifyNvRbacRoles(k8sRbacRequried, false, logging) + errs2, _ := resource.VerifyNvRbacRoleBindings(k8sRbacRequried, false, logging) + return len(errs) == 0 && len(errs2) == 0 +}
controller/kv/config.go+60 −8 modified@@ -32,7 +32,7 @@ type PauseResumeStoreWatcherFunc func(ip string, port uint16, req share.CLUSStor type ConfigHelper interface { NotifyConfigChange(endpoint string) BackupAll() - Restore() (string, bool, bool, string, error) + Restore(host share.CLUSHost, ctrler share.CLUSController) (string, bool, bool, string, error) Export(w *bufio.Writer, sections utils.Set) error Import(eps []*common.RPCEndpoint, localCtrlerID, localCtrlerIP string, loginDomainRoles access.DomainRole, importTask share.CLUSImportTask, tempToken string, revertFedRoles RevertFedRolesFunc, postImportOp PostImportFunc, pauseResumeStoreWatcher PauseResumeStoreWatcherFunc, @@ -92,15 +92,15 @@ func GetConfigHelper() ConfigHelper { } func Init(id, version, platform, flavor string, persist bool, isGroupMember FuncIsGroupMember, getConfigData FuncGetConfigKVData, - evQueue cluster.ObjectQueueInterface) { + evQueue cluster.ObjectQueueInterface, keyRotationDuration time.Duration) { evqueue = evQueue for _, ep := range cfgEndpoints { cfgEndpointMap[ep.name] = ep } newConfigHelper(id, version, persist) - clusHelper = newClusterHelper(id, version, persist) + clusHelper = newClusterHelper(id, version, persist, keyRotationDuration) orchPlatform = platform orchFlavor = flavor @@ -341,11 +341,10 @@ func (c *configHelper) BackupAll() { func restoreEP(ep *cfgEndpoint, ch chan<- error, importInfo *fedRulesRevInfo) error { var rc error + ep.backupReEncrypted = false txn := cluster.Transact() if rc = ep.restore(importInfo, txn); rc == errDone { rc = nil - } else if rc != nil { - log.WithFields(log.Fields{"endpoint": ep.name, "rc": rc}).Error() } applyTransaction(txn, nil, false, 0) @@ -374,7 +373,7 @@ func restoreEPs(eps utils.Set, ch chan error, importInfo *fedRulesRevInfo) error return err } -func (c *configHelper) Restore() (string, bool, bool, string, error) { +func (c *configHelper) Restore(host share.CLUSHost, ctrler share.CLUSController) (string, bool, bool, string, error) { log.Info() if !c.persist { @@ -425,6 +424,9 @@ func (c *configHelper) Restore() (string, bool, bool, string, error) { var err error ch := make(chan error) eps := utils.NewSetFromSliceKind(cfgEndpoints) + for _, ep := range cfgEndpoints { + ep.errRestoreKeys = utils.NewSet() + } // restore federation endpoint if rc := restoreEP(fedCfgEndpoint, nil, &importInfo); rc != nil { @@ -451,6 +453,47 @@ func (c *configHelper) Restore() (string, bool, bool, string, error) { go restoreRegistry(ch, importInfo) + epsReEncrypted := make([]string, 0, len(cfgEndpoints)) + epsMismatchedKey := make([]string, 0, len(cfgEndpoints)) + for _, ep := range cfgEndpoints { + if ep.errRestoreKeys.Cardinality() > 0 { + epsMismatchedKey = append(epsMismatchedKey, ep.errRestoreKeys.ToStringSlice()...) + ep.errRestoreKeys = nil + } else if ep.backupReEncrypted { + epsReEncrypted = append(epsReEncrypted, ep.name) + ep.backupReEncrypted = false + } + } + log.WithFields(log.Fields{"names": epsReEncrypted}).Info("re-encrypt") + messages := map[share.TLogEvent]string{ + share.CLUSEvMismatchedDEKSeed: fmt.Sprintf("Sensitive data in these kv keys cannot be restored because of decryption failure:\n%s", + strings.Join(epsMismatchedKey, ", ")), + share.CLUSEvReEncryptWithDEK: fmt.Sprintf("Sensitive data in the backup file for these endpoint(s) are re-encrypted by DEK:\n%s", + strings.Join(epsReEncrypted, ", ")), + } + for evt, eps := range map[share.TLogEvent][]string{share.CLUSEvMismatchedDEKSeed: epsMismatchedKey} { + if len(eps) > 0 { + clog := share.CLUSEventLog{ + Event: evt, + HostID: host.ID, + HostName: host.Name, + ControllerID: ctrler.ID, + ControllerName: ctrler.Name, + ReportedAt: time.Now().UTC(), + Msg: messages[evt], + } + if err := evqueue.Append(&clog); err != nil { + log.WithFields(log.Fields{"error": err, "msg": clog.Msg}).Error("evqueue.Append") + } + } + } + + c.cfgMutex.Lock() + for _, ep := range epsReEncrypted { + c.cfgChanged.Add(ep) + } + c.cfgMutex.Unlock() + ver := getBackupVersion() _ = putControlVersion(&ver) log.WithFields(log.Fields{"version": ver}).Info("Done") @@ -475,6 +518,7 @@ type configHeader struct { CreatedAt string `json:"created_at"` Sections []string `json:"sections"` ExportedFromRole string `json:"exported_from_role"` + DEKEncrypted string `json:"dek_encrypted"` } func getFedRole() (string, *share.CLUSFedRulesRevision) { @@ -508,6 +552,7 @@ func (c *configHelper) Export(w *bufio.Writer, sections utils.Set) error { sections.Add(ep.section) } } + // Fill header.Sections with order added := utils.NewSet() for _, ep := range cfgEndpoints { @@ -783,8 +828,15 @@ func (c *configHelper) importInternal(rpcEps []*common.RPCEndpoint, localCtrlerI // Value can be empty if a key was never been written when it's exported. No need to // write empty string because keys have been purged. if len(value) != 0 { - array, err := upgrade(key, []byte(value)) - if err != nil { + array, _, failToDecryptFields, err := upgrade(key, []byte(value)) + if failToDecryptFields != nil && failToDecryptFields.Cardinality() > 0 { + log.WithFields(log.Fields{"error": err, "key": key, "fields": failToDecryptFields}).Error("Failed to decrypt fields") + if importTask.FailToDecryptKeyFields == nil { + importTask.FailToDecryptKeyFields = make(map[string][]string) + } + fields := importTask.FailToDecryptKeyFields[key] + importTask.FailToDecryptKeyFields[key] = append(fields, failToDecryptFields.ToStringSlice()...) + } else if err != nil { log.WithFields(log.Fields{"error": err, "key": key, "value": value}).Error("Failed to upgrade key/value") return ErrInvalidFileFormat }
controller/kv/endpoint.go+19 −9 modified@@ -25,12 +25,14 @@ import ( type purgeFilterFunc func(epName, key string) bool type cfgEndpoint struct { - name string - key string - section string - lock string - isStore bool - purgeFilter purgeFilterFunc + name string + key string + section string + lock string + isStore bool + backupReEncrypted bool + errRestoreKeys utils.Set + purgeFilter purgeFilterFunc } const ( @@ -383,7 +385,7 @@ func (ep cfgEndpoint) backup(fedRole string) error { } // value of each key in the file is always in text format (i.e. non-gzip format). Compress it if it's >= 512k before restoring to kv -func (ep cfgEndpoint) restore(importInfo *fedRulesRevInfo, txn *cluster.ClusterTransact) error { +func (ep *cfgEndpoint) restore(importInfo *fedRulesRevInfo, txn *cluster.ClusterTransact) error { fedEndpointCfg := false source := ep.getBackupFilename() if ep.name == share.CFGEndpointFederation { @@ -444,6 +446,8 @@ func (ep cfgEndpoint) restore(importInfo *fedRulesRevInfo, txn *cluster.ClusterT // Restore key/value from files count := 0 + + log.WithFields(log.Fields{"name": ep.name}).Info() r := bufio.NewReader(f) policyZipRuleListKey := share.CLUSPolicyZipRuleListKey(share.DefaultPolicyName) for { @@ -537,11 +541,17 @@ func (ep cfgEndpoint) restore(importInfo *fedRulesRevInfo, txn *cluster.ClusterT // Value can be empty if a key was never been written when it's exported if !skip && len(value) != 0 { - array, err := upgrade(key, []byte(value)) - if err != nil { + array, wrtForReEncrypt, failToDecryptFields, err := upgrade(key, []byte(value)) + if failToDecryptFields != nil && failToDecryptFields.Cardinality() > 0 { + log.WithFields(log.Fields{"error": err, "key": key}).Error("Failed to upgrade key") + ep.errRestoreKeys.Add(key) + } else if err != nil { log.WithFields(log.Fields{"error": err, "key": key, "value": value}).Error("Failed to upgrade key/value") return ErrInvalidFileFormat } + if wrtForReEncrypt { + ep.backupReEncrypted = true + } if key == policyZipRuleListKey { applyTransaction(txn, nil, false, 0) //zip rulelist before put to cluster during restore
controller/kv/helper.go+92 −12 modified@@ -316,26 +316,44 @@ var ( ) type clusterHelper struct { - id string - version string - persist bool + id string + version string + persist bool + keyRotationDuration time.Duration } var clusHelperImpl *clusterHelper var clusHelper ClusterHelper -func newClusterHelper(id, version string, persist bool) ClusterHelper { +func newClusterHelper(id, version string, persist bool, keyRotationDuration time.Duration) ClusterHelper { clusHelperImpl = new(clusterHelper) clusHelperImpl.id = id clusHelperImpl.version = version clusHelperImpl.persist = persist + clusHelperImpl.keyRotationDuration = keyRotationDuration return clusHelperImpl } func GetClusterHelper() ClusterHelper { return clusHelperImpl } +func SetNextKeyRotationTime(keyRotationDuration time.Duration) error { + var err error + var nextRotationTime int64 + + key := share.CLUSNextKeyRotationTSKey + nextRotationTime = time.Now().Add(keyRotationDuration).Unix() + nextValue := fmt.Sprintf("%d", nextRotationTime) + if err = cluster.Put(key, []byte(nextValue)); err != nil { + log.WithFields(log.Fields{"err": err}).Error() + } else { + log.WithFields(log.Fields{"nextRotationTime": nextRotationTime}).Info() + } + + return err +} + func nvJsonUnmarshal(key string, data []byte, v any) error { var err error @@ -351,6 +369,59 @@ func nvJsonUnmarshal(key string, data []byte, v any) error { return err } +// for switching from fixed default key to variant DEK. +// [1]. If there is cloaked-by-fixed-default-key field in the json object represented by parameter 'data': +// 1-1. 'data' is unmarshalled to 'obj' and cloaked-by-fixed-default-key fields are decrypted with the fixed default key +// 1-2. 'obj' is marshalled to 'data' and sensitive fields are encrypted with variant DEK (no change on 'obj') +// 1-3. 'data' is json unmarshalled to 'obj' (i.e. keep sensitive fields cloaked-by-DEK in 'obj') +// [2]. If no field is cloaked by the fixed default key in the json object represented by parameter 'data', this function is equivalent to nvJsonUnmarshal() +// +// parameters: +// 1. key: kv key +// 2. data: kv value +// 3. obj: pointer of the object for json unmarshalling. +// when this function returns, sensitive fields in 'obj' are encrypted by DEK +func nvJsonUnmarshalReEncrypt(key string, data []byte, obj any) (bool, utils.Set, error) { + if !common.IsDEKSeedAvailable() { + _ = nvJsonUnmarshal(key, data, obj) + return false, nil, nil + } + + if obj == nil { + err := fmt.Errorf("nil target") + log.WithFields(log.Fields{"error": err, "key": key}).Error() + return false, nil, err + } + + var dec common.MigrateDecryptUnmarshaller + + err := dec.Unmarshal(data, obj) + if err == nil { + // it reaches here because dec.Unmarshal() return nil error & any of following conditions: + // 1. there is no sensitive field in 'obj'. No re-encryption required. + // 2. there is any sensitive field encrypted by the fixed default key in 'obj' that 'obj' need to be re-encrypted by DEK + // 3. all sensitive fields are encrypted with a DEK and can be DEK-decypted in 'obj'. + if dec.ReEncryptRequired { // condition #2 + // data re-encryption is required because of switching from the fixed default key to variant DEK + // notice: when this function returns, the sensitive fields in 'obj' are encrypted by DEK + var dataReEncrypted []byte + var enc common.EncryptMarshaller + if dataReEncrypted, err = enc.Marshal(obj); err == nil { + if err = nvJsonUnmarshal(key, dataReEncrypted, obj); err == nil { + return true, dec.GetFailToDecryptFields(), nil + } + } else { + log.WithFields(log.Fields{"error": err, "key": key}).Error("re-encrypt object failed") + } + } + } else { + log.WithFields(log.Fields{"error": err, "key": key}).Error("dec.Unmarshal") + } + err = nvJsonUnmarshal(key, data, obj) + + return false, dec.GetFailToDecryptFields(), err +} + func getAllSubKeys(scope, store string) utils.Set { groups := utils.NewSet() @@ -451,6 +522,7 @@ func (m clusterHelper) get(key string) ([]byte, uint64, error) { } else { var wrt bool if value, err, wrt = UpgradeAndConvert(key, value); wrt { + // wrt being true means value is out-of-date & it needs to get from kv again value, rev, err = cluster.GetRev(key) // [31, 139] is the first 2 bytes of gzip-format data if needToUnzip(key, value) { @@ -571,10 +643,15 @@ func (m clusterHelper) GetOrCreateInstallationID() (string, error) { return err } + if value, _ := cluster.Get(share.CLUSNextKeyRotationTSKey); len(value) == 0 { + _ = SetNextKeyRotationTime(m.keyRotationDuration) + } + if err = cluster.PutRev(key, []byte(id), index); err != nil { // Return the error. CASError will be automatically retried. return err } + return nil }); err != nil { return "", err @@ -761,13 +838,13 @@ func (m clusterHelper) GetDomain(name string, acc *access.AccessControl) (*share func (m clusterHelper) PutDomainIfNotExist(domain *share.CLUSDomain) error { key := share.CLUSDomainKey(domain.Name) - value, _ := enc.Marshal(domain) + value, _ := json.Marshal(domain) return cluster.PutIfNotExist(key, value, true) } func (m clusterHelper) PutDomain(domain *share.CLUSDomain, rev *uint64) error { key := share.CLUSDomainKey(domain.Name) - value, _ := enc.Marshal(domain) + value, _ := json.Marshal(domain) if rev == nil { return cluster.Put(key, value) } else { @@ -1030,19 +1107,19 @@ func (m clusterHelper) DeletePolicyRuleTxn(txn *cluster.ClusterTransact, id uint func (m clusterHelper) PutPolicyVer(s *share.CLUSGroupIPPolicyVer) error { key := share.CLUSPolicyIPRulesKey(s.Key) - value, _ := enc.Marshal(s) + value, _ := json.Marshal(s) return cluster.Put(key, value) } func (m clusterHelper) PutPolicyVerNode(s *share.CLUSGroupIPPolicyVer) error { key := share.CLUSPolicyIPRulesKeyNode(s.Key, s.NodeId) - value, _ := enc.Marshal(s) + value, _ := json.Marshal(s) return cluster.Put(key, value) } func (m clusterHelper) PutDlpVer(s *share.CLUSDlpRuleVer) error { key := share.CLUSDlpWorkloadRulesKey(s.Key) - value, _ := enc.Marshal(s) + value, _ := json.Marshal(s) return cluster.Put(key, value) } @@ -1351,7 +1428,7 @@ func (m clusterHelper) GetAllProcessProfileSubKeys(scope string) utils.Set { // Scanner func (m clusterHelper) PutScannerTxn(txn *cluster.ClusterTransact, s *share.CLUSScanner) error { key := share.CLUSScannerKey(s.ID) - value, err := enc.Marshal(s) + value, err := json.Marshal(s) if err != nil { return err } @@ -2151,8 +2228,11 @@ func (m clusterHelper) GetAdmissionCertRev(svcName string) (*share.CLUSAdmission func (m clusterHelper) GetObjectCertRev(cn string) (*share.CLUSX509Cert, uint64, error) { key := share.CLUSObjectCertKey(cn) - value, rev, err := cluster.GetRev(key) - if err != nil || value == nil { + value, rev, err := m.get(key) + if err == nil && value == nil { + err = fmt.Errorf("cert %s is not set", key) + } + if err != nil { log.WithFields(log.Fields{"cn": cn, "error": err}).Error() return nil, rev, err } else {
controller/kv/upgrade.go+133 −67 modified@@ -412,9 +412,24 @@ func upgradeAdmissionCert(value []byte) (*share.CLUSAdmissionCertCloaked, bool, return nil, false, false } -func doUpgrade(key string, value []byte) (interface{}, bool) { +// This is called when +// 1. restore persisted config into kv store +// 2. import configurations into kv store +// 3. read from kv store +// 4. get notified by kv changes. +// +// returned values: +// #1. pointer to the upgraded object(sensitive fields in 'obj' are encrypted by DEK). nil if no upgrade happened +// #2. true to advise caller to write the upgraded object back to data source +// #3. true to advise caller to write the re-marshalled(because of using DEK) object back to data source +// #4. error: ErrMismatchedKey or other error +func doUpgrade(key string, value []byte) (interface{}, bool, bool, utils.Set, error) { object := share.CLUSObjectKey2Object(key) + var err error + var wrtForReEncrypt bool + var failToDecryptFields utils.Set + switch object { case "config": config := share.CLUSConfigKey2Config(key) @@ -423,71 +438,74 @@ func doUpgrade(key string, value []byte) (interface{}, bool) { case share.CFGEndpointUser: var user share.CLUSUser _ = nvJsonUnmarshal(key, value, &user) - if upd, wrt := upgradeUser(&user); upd { - return &user, wrt + if upd, wrtForUpgrade := upgradeUser(&user); upd { + return &user, wrtForUpgrade, false, nil, nil } case share.CFGEndpointSystem: var cfg share.CLUSSystemConfig - _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeSystemConfig(&cfg); upd { - return &cfg, wrt + if wrtForReEncrypt, failToDecryptFields, err = nvJsonUnmarshalReEncrypt(key, value, &cfg); err == nil { + if upd, wrtForUpgrade := upgradeSystemConfig(&cfg); upd || wrtForReEncrypt { + return &cfg, wrtForUpgrade, wrtForReEncrypt, failToDecryptFields, nil + } } case share.CFGEndpointServer: var cfg share.CLUSServer - _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeServer(&cfg); upd { - return &cfg, wrt + if wrtForReEncrypt, failToDecryptFields, err = nvJsonUnmarshalReEncrypt(key, value, &cfg); err == nil { + if upd, wrtForUpgrade := upgradeServer(&cfg); upd || wrtForReEncrypt { + return &cfg, wrtForUpgrade, wrtForReEncrypt, failToDecryptFields, nil + } } case share.CFGEndpointGroup: var cfg share.CLUSGroup _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeGroup(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeGroup(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } case share.CFGEndpointPolicy: if share.CLUSIsPolicyRuleKey(key) { var cfg share.CLUSPolicyRule _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradePolicyRule(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradePolicyRule(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } } else if share.CLUSIsPolicyZipRuleListKey(key) { var cfg []*share.CLUSRuleHead _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradePolicyRuleHead(cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradePolicyRuleHead(cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } } case share.CFGEndpointRegistry: var cfg share.CLUSRegistryConfig - _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeRegistry(&cfg); upd { - return &cfg, wrt + if wrtForReEncrypt, failToDecryptFields, err = nvJsonUnmarshalReEncrypt(key, value, &cfg); err == nil { + if upd, wrtForUpgrade := upgradeRegistry(&cfg); upd || wrtForReEncrypt { + return &cfg, wrtForUpgrade, wrtForReEncrypt, failToDecryptFields, nil + } } case share.CFGEndpointProcessProfile: var cfg share.CLUSProcessProfile _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeProcessProfile(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeProcessProfile(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } case share.CFGEndpointFileMonitor: var cfg share.CLUSFileMonitorProfile _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeFileMonitorProfile(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeFileMonitorProfile(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } case share.CFGEndpointDlpGroup: var cfg share.CLUSDlpGroup _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeDlpGroup(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeDlpGroup(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } case share.CFGEndpointDlpRule: var cfg share.CLUSDlpSensor _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeDlpSensor(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeDlpSensor(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } case share.CFGEndpointAdmissionControl, share.CFGEndpointCrd: scope := share.CLUSPolicyKey2AdmCfgPolicySubkey(key, false) @@ -496,53 +514,62 @@ func doUpgrade(key string, value []byte) (interface{}, bool) { if token == share.CLUSAdmissionCfgState { var state share.CLUSAdmissionState _ = nvJsonUnmarshal(key, value, &state) - if upd, wrt := upgradeAdmCtrlState(config, &state); upd { - return &state, wrt + if upd, wrtForUpgrade := upgradeAdmCtrlState(config, &state); upd { + return &state, wrtForUpgrade, false, nil, nil } } else { if config == share.CFGEndpointAdmissionControl { if token == share.CLUSAdmissionCfgRule { var rule share.CLUSAdmissionRule _ = nvJsonUnmarshal(key, value, &rule) - if upd, wrt := upgradeAdmCtrlRule(&rule); upd { - return &rule, wrt + if upd, wrtForUpgrade := upgradeAdmCtrlRule(&rule); upd { + return &rule, wrtForUpgrade, false, nil, nil } } else if token == share.CLUSAdmissionCfgRuleList { var cfg []*share.CLUSRuleHead _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeRuleHead(cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeRuleHead(cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } } else if token == share.CLUSAdmissionCfgCert { var cert share.CLUSAdmissionCertCloaked - err := dec.Unmarshal(value, &cert) - if err != nil || !cert.Cloaked { + var dec common.MigrateDecryptUnmarshaller + err2 := dec.Unmarshal(value, &cert) + if err2 != nil || !cert.Cloaked { if cert, upd, wrt := upgradeAdmissionCert(value); upd { - return cert, wrt + return cert, wrt, false, nil, nil + } + } else if dec.ReEncryptRequired { + if dataReEncrypted, err2 := enc.Marshal(&cert); err2 == nil { + _ = nvJsonUnmarshal(key, dataReEncrypted, &cert) + return &cert, false, true, nil, nil + } else { + log.WithFields(log.Fields{"error": err2, "key": key}).Error("re-encrypt object failed") } } + err = err2 } } } } else if config == share.CFGEndpointCrd && scope == resource.NvSecurityRuleKind { var cfg share.CLUSCrdSecurityRule _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeCrdSecurityRule(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeCrdSecurityRule(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } } case share.CFGEndpointResponseRule: if share.CLUSIsPolicyRuleKey(key) { var cfg share.CLUSResponseRule _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeResponseRule(&cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeResponseRule(&cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } } else if share.CLUSIsPolicyRuleListKey(key) { var cfg []*share.CLUSRuleHead _ = nvJsonUnmarshal(key, value, &cfg) - if upd, wrt := upgradeRuleHead(cfg); upd { - return &cfg, wrt + if upd, wrtForUpgrade := upgradeRuleHead(cfg); upd { + return &cfg, wrtForUpgrade, false, nil, nil } } case share.CFGEndpointVulnerability: @@ -555,7 +582,7 @@ func doUpgrade(key string, value []byte) (interface{}, bool) { upd = true } if upd { - return &cfg, upd + return &cfg, upd, false, nil, nil } } } @@ -569,28 +596,47 @@ func doUpgrade(key string, value []byte) (interface{}, bool) { upd = true } if upd { - return &cfg, upd + return &cfg, upd, false, nil, nil } } } + case share.CFGEndpointFederation: + if key == share.CLUSFedKey(share.CLUSFedMembershipSubKey) { + var cfg share.CLUSFedMembership + if wrtForReEncrypt, failToDecryptFields, err = nvJsonUnmarshalReEncrypt(key, value, &cfg); err == nil && wrtForReEncrypt { + return &cfg, false, wrtForReEncrypt, failToDecryptFields, nil + } + } else if share.CLUSFedKey2CfgKey(key) == share.CLUSFedClustersSubKey { + var cfg share.CLUSFedJointClusterInfo + if wrtForReEncrypt, failToDecryptFields, err = nvJsonUnmarshalReEncrypt(key, value, &cfg); err == nil && wrtForReEncrypt { + return &cfg, false, wrtForReEncrypt, failToDecryptFields, nil + } + } + } + case "cert": + var cert share.CLUSX509Cert + if wrtForReEncrypt, failToDecryptFields, err = nvJsonUnmarshalReEncrypt(key, value, &cert); err == nil && wrtForReEncrypt { + return &cert, false, wrtForReEncrypt, failToDecryptFields, nil } } - return nil, false + return nil, false, false, failToDecryptFields, err } -// This is called when we restore the persisted config into kv store -func upgrade(key string, value []byte) ([]byte, error) { +// This is called when we restore the persisted config or import configurations into kv store +func upgrade(key string, value []byte) ([]byte, bool, utils.Set, error) { if len(value) == 0 { - return value, nil + return value, false, nil, nil } - v, _ := doUpgrade(key, value) + v, _, wrtForReEncrypt, failToDecryptFields, err := doUpgrade(key, value) if v == nil { - return value, nil + return value, false, failToDecryptFields, err } - return json.Marshal(v) + data, err := json.Marshal(v) + + return data, wrtForReEncrypt, failToDecryptFields, err } // This is called whenever we read from kv store or get notified by kv changes. @@ -599,8 +645,11 @@ func UpgradeAndConvert(key string, value []byte) ([]byte, error, bool) { return value, nil, false } var v interface{} - var wrt bool + var wrtForUpgrade bool + var wrtForReEncrypt bool var policyListKey bool + var needToUncloak bool + var failToDecryptFields utils.Set if key == share.CLUSPolicyZipRuleListKey(share.DefaultPolicyName) { policyListKey = true @@ -612,7 +661,10 @@ func UpgradeAndConvert(key string, value []byte) ([]byte, error, bool) { return value, nil, false } } - v, wrt = doUpgrade(key, value) + v, wrtForUpgrade, wrtForReEncrypt, failToDecryptFields, _ = doUpgrade(key, value) + // v being nil means no need to upgrade/convert the obj represented by value at all + wrt := wrtForUpgrade || wrtForReEncrypt + // wrt means need to write v to kv or not (sensitive fields in v are still encrypted) if v != nil && wrt { var err error @@ -624,6 +676,23 @@ func UpgradeAndConvert(key string, value []byte) ([]byte, error, bool) { } else { // Write back to the cluster if needed. err = cluster.Put(key, newv) + if err == nil && wrtForReEncrypt && key == share.CLUSConfigSystemKey && (failToDecryptFields == nil || failToDecryptFields.Cardinality() == 0) { + if cfg, ok := v.(*share.CLUSSystemConfig); ok && cfg != nil { + cfgBackup := share.CLUSSystemConfigEncMigrated{ + EncMigratedSystemConfig: string(newv), + EncDataExpirationTime: time.Now().UTC().Add(time.Hour), + } + value, err := json.Marshal(&cfgBackup) + if err == nil { + if err = cluster.PutBinary(share.CLUSSystemEncMigratedKey, value); err == nil { + log.Info("backup encryption-migrated config") + } + } + if err != nil { + log.WithFields(log.Fields{"err": err}).Error("failed to backup encryption-migrated config") + } + } + } } if err != nil { log.WithFields(log.Fields{"key": key}).Error(err) @@ -646,9 +715,7 @@ func UpgradeAndConvert(key string, value []byte) ([]byte, error, bool) { _ = nvJsonUnmarshal(key, value, &r) v = &r } - if err := dec.Uncloak(v); err != nil { - log.WithFields(log.Fields{"err": err, "key": key}).Error("Uncloak") - } + needToUncloak = true } } case "config": @@ -661,37 +728,36 @@ func UpgradeAndConvert(key string, value []byte) ([]byte, error, bool) { _ = nvJsonUnmarshal(key, value, &cfg) v = &cfg } - if err := dec.Uncloak(v); err != nil { - log.WithFields(log.Fields{"err": err, "key": key}).Error("Uncloak") - } + needToUncloak = true case share.CFGEndpointServer: if v == nil { var cfg share.CLUSServer _ = nvJsonUnmarshal(key, value, &cfg) v = &cfg } - if err := dec.Uncloak(v); err != nil { - log.WithFields(log.Fields{"err": err, "key": key}).Error("Uncloak") - } + needToUncloak = true case share.CFGEndpointRegistry: if v == nil { var cfg share.CLUSRegistryConfig _ = nvJsonUnmarshal(key, value, &cfg) v = &cfg } - if err := dec.Uncloak(v); err != nil { - log.WithFields(log.Fields{"err": err, "key": key}).Error("Uncloak") - } + needToUncloak = true case share.CFGEndpointCloud: if v == nil { // Currently the only data structure var cfg share.CLUSAwsProjectCfg _ = nvJsonUnmarshal(key, value, &cfg) v = &cfg } - if err := dec.Uncloak(v); err != nil { - log.WithFields(log.Fields{"err": err, "key": key}).Error("Uncloak") - } + needToUncloak = true + } + } + + if v != nil && needToUncloak { + // sensitive fields in v are still encrypted + if err := dec.Uncloak(v); err != nil { + log.WithFields(log.Fields{"err": err, "key": key}).Error("Uncloak") } }
controller/orch.go+7 −4 modified@@ -154,7 +154,7 @@ func (c *orchConn) cbResourceWatcher(rt string, event string, res interface{}, o } default: logFields := log.Fields{"event": event, "type": rt} - if rt != resource.RscTypeConfigMap { + if rt != resource.RscTypeConfigMap && rt != resource.RscTypeSecret { logFields["object"] = res } k8sResLog.WithFields(logFields).Debug("Event received") @@ -203,11 +203,14 @@ func (c *orchConn) Start(ocImageRegistered bool, cspType share.TCspType) { resource.RscTypeValidatingWebhookConfiguration, resource.RscTypePersistentVolumeClaim, resource.RscTypeConfigMap} for _, r := range rscTypes { if err := global.ORCH.StartWatchResource(r, k8s.AllNamespaces, c.cbResourceWatcher, nil); err != nil { - log.WithFields(log.Fields{"error": err}).Error("StartWatchResource") + log.WithFields(log.Fields{"type": r, "error": err}).Error("StartWatchResource") } } - if err := global.ORCH.StartWatchResource(resource.RscTypeDeployment, Ctrler.Domain, c.cbResourceWatcher, nil); err != nil { - log.WithFields(log.Fields{"error": err}).Error("StartWatchResource") + + for _, r := range []string{resource.RscTypeDeployment, resource.RscTypeSecret} { + if err := global.ORCH.StartWatchResource(r, Ctrler.Domain, c.cbResourceWatcher, nil); err != nil { + log.WithFields(log.Fields{"type": r, "error": err}).Error("StartWatchResource") + } } rscTypes = []string{
controller/resource/kubernetes_rbac.go+5 −2 modified@@ -1975,8 +1975,7 @@ func GetSaFromJwtToken(tokenStr string) (string, error) { return sa, err } -func GetNvCtrlerServiceAccount(objFunc common.CacheEventFunc) { - cacheEventFunc = objFunc +func GetNvCtrlerServiceAccount() { // controller pod runs as "controller" sa if it's deployed with least privilge enabled filePath := "/var/run/secrets/kubernetes.io/serviceaccount/token" if data, err := os.ReadFile(filePath); err == nil { @@ -1994,6 +1993,10 @@ func GetNvCtrlerServiceAccount(objFunc common.CacheEventFunc) { log.WithFields(log.Fields{"nvControllerSA": ctrlerSubjectWanted}).Info() } +func SetCacheEventFunc(objFunc common.CacheEventFunc) { + cacheEventFunc = objFunc +} + func getSubjectsString(ns string, subjects []string) string { subjectSet := utils.NewSet() fullSubjects := make([]string, 0, len(subjects))
controller/resource/kubernetes_resource.go+135 −10 modified@@ -133,6 +133,7 @@ const ( nvCspUsageRoleBinding = nvCspUsageRole nvBootstrapSecret = "neuvector-bootstrap-secret" + nvStoreSecret = "neuvector-store-secret" secretKeyResetByNV = "resetByNV" secretKeyBootstrapPassword = "bootstrapPassword" @@ -707,7 +708,7 @@ var resourceMakers map[string]k8sResource = map[string]k8sResource{ "v1", func() metav1.Object { return new(corev1.Secret) }, func() metav1.ListInterface { return new(corev1.SecretList) }, - nil, // xlateSecret, + xlateSecret, nil, }, }, @@ -1204,6 +1205,10 @@ func xlateConfigMap(obj metav1.Object) (string, interface{}) { return "", nil } +func xlateSecret(obj metav1.Object) (string, interface{}) { + return string(obj.GetUID()), obj +} + // func xlateMutatingWebhookConfiguration(obj metav1.Object) (string, interface{}) { // var name string // var guid string @@ -2243,27 +2248,29 @@ func xlatePersistentVolumeClaim(obj metav1.Object) (string, interface{}) { return "", nil } -func retrieveSecretData(secretName, key string) ([]byte, bool, error) { - var objSecret *corev1.Secret - var foundSecret bool +func retrieveSecretData(secretName, key string) ([]byte, map[string][]byte, string, bool, error) { obj, err := global.ORCH.GetResource(RscTypeSecret, NvAdmSvcNamespace, secretName) if obj != nil && err == nil { - objSecret, foundSecret = obj.(*corev1.Secret) + objSecret, foundSecret := obj.(*corev1.Secret) if foundSecret && objSecret != nil { if objSecret.Data != nil { - if v, ok := objSecret.Data[key]; ok { - return v, foundSecret, nil + if key != "" { + if v, ok := objSecret.Data[key]; ok { + return v, nil, "", foundSecret, nil + } + } else { + return nil, objSecret.Data, objSecret.ResourceVersion, foundSecret, nil } } } else { - err = errors.New("type conversion failed") + err = fmt.Errorf("type conversion failed") } } if err != nil { log.WithFields(log.Fields{"err": err, "secretName": secretName}).Error() } - return nil, foundSecret, err + return nil, nil, "", false, err } func GenRandomString(length int) (string, error) { @@ -2281,7 +2288,7 @@ func GenRandomString(length int) (string, error) { } func RetrieveBootstrapPassword() (string, error) { - data, foundSecret, err := retrieveSecretData(nvBootstrapSecret, secretKeyBootstrapPassword) + data, _, _, foundSecret, err := retrieveSecretData(nvBootstrapSecret, secretKeyBootstrapPassword) if err == nil && len(data) > 0 { return string(data), nil } @@ -2329,6 +2336,124 @@ func RetrieveBootstrapPassword() (string, error) { return "", err } +// Caller must acquire/lease CLUSStoreSecretKey lock befor/after calling this function +func RetrieveStorePassphrases() (common.EncKeys, string, string, error) { + var err error + var msg string + var currEncKeyIdx uint64 + var currEncKeyVer string + + _, secretData, rscVersion, foundSecret, _ := retrieveSecretData(nvStoreSecret, "") + encKeys := make(common.EncKeys, len(secretData)) + for k, v := range secretData { + if ui64, err := strconv.ParseUint(k, 10, 64); err == nil { + if ui64 > currEncKeyIdx { + currEncKeyIdx = ui64 + } + encKeys[k] = v + } + } + + if len(encKeys) == 0 { + var action string + randomPass := make([]byte, common.DekSeedLength) + if _, err := rand.Read(randomPass); err != nil { + return nil, "", "", fmt.Errorf("failed to generate random password: %w", err) + } + encKeys = make(common.EncKeys, 1) + currEncKeyIdx = 1 + currEncKeyVer = fmt.Sprintf("%v", currEncKeyIdx) + encKeys[currEncKeyVer] = randomPass + objSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nvStoreSecret, + Namespace: NvAdmSvcNamespace, + }, + Data: map[string][]byte{ + strconv.Itoa(int(currEncKeyIdx)): randomPass, + secretKeyResetByNV: []byte("true"), + }, + Type: corev1.SecretTypeOpaque, + } + if !foundSecret { + err = global.ORCH.AddResource(RscTypeSecret, &objSecret) + action = "created" + } else { + objSecret.ResourceVersion = rscVersion + err = global.ORCH.UpdateResource(RscTypeSecret, &objSecret) + action = "updated" + } + if err != nil { + log.WithFields(log.Fields{"err": err, "rscVersion": rscVersion, "foundSecret": foundSecret}).Error() + } else { + msg = fmt.Sprintf("Kubernetes secret %s is %s", nvStoreSecret, action) + } + } else { + err = nil + } + + return encKeys, fmt.Sprintf("%d", currEncKeyIdx), msg, err +} + +func AddStorePassphrase() error { + var err error + var currEncKeyIdx uint64 + + _, secretData, rscVersion, foundSecret, _ := retrieveSecretData(nvStoreSecret, "") + for k := range secretData { + if ui64, err := strconv.ParseUint(k, 10, 64); err == nil { + if ui64 > currEncKeyIdx { + currEncKeyIdx = ui64 + } + } + } + if secretData == nil { + secretData = make(map[string][]byte, 2) + secretData[secretKeyResetByNV] = []byte("true") + } + + randomPass := make([]byte, common.DekSeedLength) + if _, err := rand.Read(randomPass); err != nil { + return fmt.Errorf("failed to generate random password: %w", err) + } + currEncKeyIdx++ + keyVersion := fmt.Sprintf("%d", currEncKeyIdx) + secretData[keyVersion] = randomPass + objSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nvStoreSecret, + Namespace: NvAdmSvcNamespace, + }, + Data: secretData, + Type: corev1.SecretTypeOpaque, + } + if !foundSecret { + err = global.ORCH.AddResource(RscTypeSecret, &objSecret) + } else { + objSecret.ResourceVersion = rscVersion + err = global.ORCH.UpdateResource(RscTypeSecret, &objSecret) + } + if err == nil { + err = common.AddAesGcmKey(keyVersion, randomPass) + } + if err != nil { + log.WithFields(log.Fields{"err": err, "foundSecret": foundSecret}).Error() + } + + return err +} + +func IsStoreSecretUpdatedByNV() bool { + secretResetByNV := false + + data, _, _, found, err := retrieveSecretData(nvStoreSecret, secretKeyResetByNV) + if err == nil && found && string(data) == "true" { + secretResetByNV = true + } + + return secretResetByNV +} + func GetNvControllerPodsNumber() { var requestMemory string var limitMemory string
controller/rest/auth.go+42 −18 modified@@ -154,6 +154,7 @@ const ( userTooMany userKeyError userNoPlatformAuth + userTokenGenError ) // const ( @@ -207,7 +208,10 @@ func newLoginSessionFromUser(user *share.CLUSUser, domainRoles access.DomainRole remote, mainSessionID, mainSessionUser string, sso *SsoSession) (*loginSession, int) { // Note: JWT keys should be loaded in initJWTSignKey() before calling this function. - id, token, claims := jwtGenerateToken(user, domainRoles, extraDomainPermits, remote, mainSessionID, mainSessionUser, sso) + id, token, claims, err := jwtGenerateToken(user, domainRoles, extraDomainPermits, remote, mainSessionID, mainSessionUser, sso) + if err != nil { + return nil, userTokenGenError + } now := user.LastLoginAt // Already updated s := &loginSession{ id: id, @@ -1279,14 +1283,16 @@ func resetFedJointKeys() { func setJointKeysInCache(callerFedRole string, jointCluster *share.CLUSFedJointClusterInfo) error { var err error var data []byte + var token string var rsaPrivateKey *rsa.PrivateKey var rsaPublicKey *rsa.PublicKey if data, err = base64.StdEncoding.DecodeString(jointCluster.ClientKey); err == nil { if rsaPrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(data); err == nil && rsaPrivateKey != nil { if data, err = base64.StdEncoding.DecodeString(jointCluster.ClientCert); err == nil { if rsaPublicKey, err = jwt.ParseRSAPublicKeyFromPEM(data); err == nil && rsaPublicKey != nil { - if token := jwtGenFedPingToken(callerFedRole, jointCluster.ID, jointCluster.Secret, rsaPrivateKey); token != "" { - if _, err := jwtValidateToken(token, jointCluster.Secret, rsaPublicKey); err == nil { + token, err = jwtGenFedPingToken(callerFedRole, jointCluster.ID, jointCluster.Secret, rsaPrivateKey) + if token != "" && err == nil { + if _, err = jwtValidateToken(token, jointCluster.Secret, rsaPublicKey); err == nil { _setFedJointPrivateKey(jointCluster.ID, rsaPrivateKey) if callerFedRole == api.FedRoleJoint { _setFedJointPublicKey(rsaPublicKey) @@ -1352,7 +1358,11 @@ func jwtValidateToken(encryptedToken, secret string, rsaPublicKey *rsa.PublicKey } if secret == "" { - tokenString = utils.DecryptUserToken(encryptedToken, []byte(installID)) + if tokenString, err = utils.DecryptUserToken(encryptedToken, []byte(installID)); err != nil { + err = fmt.Errorf("failed to decrypt token: %w", err) + log.WithError(err).Error() + return nil, err + } } else { tokenString = utils.DecryptSensitive(encryptedToken, []byte(secret)) } @@ -1447,13 +1457,13 @@ func jwtValidateFedJoinTicket(encryptedTicket, secret string) error { // permits is for Rancher SSO only func jwtGenerateToken(user *share.CLUSUser, domainRoles access.DomainRole, extraDomainPermits access.DomainPermissions, - remote, mainSessionID, mainSessionUser string, sso *SsoSession) (string, string, *tokenClaim) { + remote, mainSessionID, mainSessionUser string, sso *SsoSession) (string, string, *tokenClaim, error) { id := utils.GetRandomID(idLength, "") installID, err := clusHelper.GetInstallationID() if err != nil { log.WithError(err).Error("failed to get installation ID") - return "", "", &tokenClaim{} + return "", "", nil, err } now := time.Now() c := tokenClaim{ @@ -1494,10 +1504,15 @@ func jwtGenerateToken(user *share.CLUSUser, domainRoles access.DomainRole, extra jwtCert := GetJWTSigningKey() token := jwt.NewWithClaims(jwt.SigningMethodRS256, c) tokenString, err := token.SignedString(jwtCert.jwtPrivateKey) - if tokenString == "" || err != nil { - log.WithFields(log.Fields{"err": err}).Error() + if err != nil { + log.WithFields(log.Fields{"err": err}).Error("failed to sign token") + return "", "", nil, err + } + if tokenString, err = utils.EncryptUserToken(tokenString, []byte(installID)); err != nil { + log.WithError(err).Error("failed to encrypt token") + return "", "", nil, err } - return id, utils.EncryptUserToken(tokenString, []byte(installID)), &c + return id, tokenString, &c, nil } func jwtGenFedJoinToken(masterCluster *api.RESTFedMasterClusterInfo, duration time.Duration) []byte { @@ -1521,7 +1536,7 @@ func jwtGenFedTicket(secret string, duration time.Duration) string { return utils.EncryptSensitive(string(tokenBytes), []byte(secret)) } -func _genFedJwtToken(c *tokenClaim, callerFedRole, clusterID, secret string, rsaPrivateKey *rsa.PrivateKey) string { +func _genFedJwtToken(c *tokenClaim, callerFedRole, clusterID, secret string, rsaPrivateKey *rsa.PrivateKey) (string, error) { // rsaPrivateKey being non-nil is for validating new public/private keys purpose token := jwt.NewWithClaims(jwt.SigningMethodRS256, *c) var privateKey *rsa.PrivateKey @@ -1537,24 +1552,33 @@ func _genFedJwtToken(c *tokenClaim, callerFedRole, clusterID, secret string, rsa } else { privateKey = rsaPrivateKey } + + var tokenString string + var err error if privateKey != nil { - tokenString, _ := token.SignedString(privateKey) - return utils.EncryptSensitive(tokenString, []byte(secret)) + tokenString, err = token.SignedString(privateKey) + if tokenString == "" || err != nil { + log.WithFields(log.Fields{"id": clusterID, "err": err}).Error("failed to sign token") + return "", err + } + return utils.EncryptSensitive(tokenString, []byte(secret)), nil } else { - log.WithFields(log.Fields{"id": clusterID}).Error("empty private key") + err = errors.New("empty private key") + log.WithFields(log.Fields{"id": clusterID, "err": err}).Error() } - return "" + + return "", err } -func jwtGenFedMasterToken(user *share.CLUSUser, login *loginSession, clusterID, secret string) string { +func jwtGenFedMasterToken(user *share.CLUSUser, login *loginSession, clusterID, secret string) (string, error) { if !login.hasFedPermission() { // caller needs to have fed role or fed permission - return "" + return "", common.ErrObjectAccessDenied } if user.RemoteRolePermits == nil || (len(user.RemoteRolePermits.DomainRole) == 0 && len(user.RemoteRolePermits.ExtraPermits) == 0) { - return "" + return "", common.ErrObjectAccessDenied } id := utils.GetRandomID(idLength, "") @@ -1585,7 +1609,7 @@ func jwtGenFedMasterToken(user *share.CLUSUser, login *loginSession, clusterID, return _genFedJwtToken(&c, api.FedRoleMaster, clusterID, secret, nil) } -func jwtGenFedPingToken(callerFedRole, clusterID, secret string, rsaPrivateKey *rsa.PrivateKey) string { +func jwtGenFedPingToken(callerFedRole, clusterID, secret string, rsaPrivateKey *rsa.PrivateKey) (string, error) { // rsaPrivateKey being non-nil is for validating new public/private keys purpose id := utils.GetRandomID(idLength, "")
controller/rest/auth_test.go+4 −1 modified@@ -1043,7 +1043,10 @@ func TestJWTSignValidate(t *testing.T) { } remote := "10.1.2.3" - _, tokenString, _ := jwtGenerateToken(user, roles, nil, remote, "", "", nil) + _, tokenString, _, err := jwtGenerateToken(user, roles, nil, remote, "", "", nil) + if err != nil { + t.Errorf("Failed to generate jwt token: user=%+v error=%+v", user, err) + } token, _ := jwtValidateToken(tokenString, "", nil) if token.Fullname != user.Fullname {
controller/rest/federation.go+85 −58 modified@@ -47,9 +47,10 @@ type cmdResponse struct { } type tNvHttpClient struct { - httpClient *http.Client - proxyUrlStr string // non-empty for connection thru proxy - basicAuth string // non-empty for proxy that requires auth + httpClient *http.Client // HTTP client with TLS verification enabled. + insecureHttpClient *http.Client // HTTP client with TLS verification skipped. + proxyUrlStr string // non-empty for connection thru proxy + basicAuth string // non-empty for proxy that requires auth } const ( @@ -414,21 +415,40 @@ func getProxyURL(r *http.Request) (*url.URL, error) { return nil, nil } -func createHttpClient(proxyOption int8, timeout time.Duration) (*http.Client, string, string) { - var proxyUrlStr string - var basicAuth string - var proxy share.CLUSProxy - +func createHttpClient(basicAuth string, timeout time.Duration, tlsInsecureSkipVerify bool) *http.Client { // refer to http.DefaultTransport transport := &http.Transport{ Proxy: getProxyURL, TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: tlsInsecureSkipVerify, }, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, DisableCompression: true, } + if basicAuth != "" { + transport.ProxyConnectHeader = http.Header{} + transport.ProxyConnectHeader.Add("Proxy-Authorization", basicAuth) + } + httpClient := &http.Client{ + Transport: transport, + Timeout: timeout, + } + jar, err := cookiejar.New(nil) + if err != nil { + log.WithFields(log.Fields{"tlsInsecureSkipVerify": tlsInsecureSkipVerify, "err": err}).Error("creating cookie jar") + } else { + httpClient.Jar = jar + } + + return httpClient +} + +func createNvHttpClient(proxyOption int8, timeout time.Duration) *tNvHttpClient { + var proxyUrlStr string + var basicAuth string + var proxy share.CLUSProxy + if proxyOption != const_no_proxy { _sysProxyMutex.RLock() if proxyOption == const_http_proxy { @@ -443,22 +463,15 @@ func createHttpClient(proxyOption int8, timeout time.Duration) (*http.Client, st if proxy.Username != "" { auth := fmt.Sprintf("%s:%s", proxy.Username, proxy.Password) basicAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) - transport.ProxyConnectHeader = http.Header{} - transport.ProxyConnectHeader.Add("Proxy-Authorization", basicAuth) } } - httpClient := &http.Client{ - Transport: transport, - Timeout: timeout, - } - jar, err := cookiejar.New(nil) - if err != nil { - log.WithFields(log.Fields{"proxyOption": proxyOption, "err": err}).Error("creating cookie jar") - } else { - httpClient.Jar = jar - } - return httpClient, proxyUrlStr, basicAuth + return &tNvHttpClient{ + httpClient: createHttpClient(basicAuth, timeout, false), // http.Transport.TLSClientConfig.InsecureSkipVerify is false + insecureHttpClient: createHttpClient(basicAuth, timeout, true), // http.Transport.TLSClientConfig.InsecureSkipVerify is true + proxyUrlStr: proxyUrlStr, + basicAuth: basicAuth, + } } func getProxyOptions(id string, useProxy int8) []int8 { @@ -511,7 +524,6 @@ func getProxyOptions(id string, useProxy int8) []int8 { func sendRestRequest(idTarget string, method, urlStr, token, cntType, jointTicket, jointID string, cookie *http.Cookie, body []byte, logError bool, specificProxy *int8, acc *access.AccessControl) ([]byte, int, bool, error) { - var useProxy int8 var data []byte var statusCode int @@ -540,19 +552,13 @@ func sendRestRequest(idTarget string, method, urlStr, token, cntType, jointTicke nvHttpClient = _nvHttpClients[proxyOption] _httpClientMutex.RUnlock() if nvHttpClient == nil { - httpClient, proxyUrlStr, basicAuth := createHttpClient(proxyOption, clusterAuthTimeout) - nvHttpClient = &tNvHttpClient{ - httpClient: httpClient, - proxyUrlStr: proxyUrlStr, - basicAuth: basicAuth, - } + nvHttpClient = createNvHttpClient(proxyOption, clusterAuthTimeout) _httpClientMutex.Lock() _nvHttpClients[proxyOption] = nvHttpClient _httpClientMutex.Unlock() } - if data, statusCode, err = sendRestReqInternal(nvHttpClient, method, urlStr, token, cntType, + if data, statusCode, err = sendRestReqInternal(nvHttpClient, idTarget, method, urlStr, token, cntType, jointTicket, jointID, proxyOption, cookie, body, logError); err == nil { - _httpClientMutex.Lock() _proxyOptionHistory[idTarget] = proxyOption _httpClientMutex.Unlock() @@ -566,14 +572,17 @@ func sendRestRequest(idTarget string, method, urlStr, token, cntType, jointTicke return data, statusCode, usedProxy, err } -func sendRestReqInternal(nvHttpClient *tNvHttpClient, method, urlStr, token, cntType, jointTicket, jointID string, +func sendRestReqInternal(nvHttpClient *tNvHttpClient, idTarget, method, urlStr, token, cntType, jointTicket, jointID string, proxyOption int8, cookie *http.Cookie, body []byte, logError bool) ([]byte, int, error) { - - var httpClient *http.Client = nvHttpClient.httpClient + var httpClient *http.Client = nvHttpClient.insecureHttpClient var req *http.Request var gzipped bool var err error + if idTarget == "telemetry" && !cctx.TeleSkipTlsVerification { + httpClient = nvHttpClient.httpClient + } + if jointTicket != "" && jointID != "" { if len(body) > gzipThreshold { body = utils.GzipBytes(body) @@ -636,7 +645,23 @@ func sendRestReqInternal(nvHttpClient *tNvHttpClient, method, urlStr, token, cnt } defer resp.Body.Close() - data, err := io.ReadAll(resp.Body) + var data []byte + if idTarget == "telemetry" { + // because neuvector_versions.json only has one version entry, the expected len of returned neuvector_versions.json should be < 256 + var dataBuffer [256]byte + reader := io.LimitReader(resp.Body, int64(len(dataBuffer))) + n, err2 := reader.Read(dataBuffer[:]) + if err2 != io.EOF { + err = fmt.Errorf("unexpected %d byes of data read, error: %v", n, err2) + if _, err2 = io.Copy(io.Discard, resp.Body); err2 != nil { + log.WithFields(log.Fields{"error": err2}).Error("Discard remaining data fail") + } + } else { + data = dataBuffer[:n] + } + } else { + data, err = io.ReadAll(resp.Body) + } if err != nil { log.WithFields(log.Fields{"url": urlStr, "status": resp.Status, "proxyOption": proxyOption}).Error("Read data fail") return nil, 0, err @@ -696,17 +721,17 @@ func RestConfig(cmd, interval uint32, param1 interface{}, param2 interface{}) er // no need to reconstruct a new http client when only proxy url changes. // however, if proxy auth changes, we need to reconstruct a new http client because transport ProxyConnectHeader is a map if nvHttpClient == nil || newBasicAuth != nvHttpClient.basicAuth { - newHttpClient, proxyUrlStr, basicAuth := createHttpClient(proxyOption, clusterAuthTimeout) - newNvHttpClient := &tNvHttpClient{ - httpClient: newHttpClient, - proxyUrlStr: proxyUrlStr, - basicAuth: basicAuth, - } + newNvHttpClient := createNvHttpClient(proxyOption, clusterAuthTimeout) _httpClientMutex.Lock() _nvHttpClients[proxyOption] = newNvHttpClient _httpClientMutex.Unlock() - if nvHttpClient != nil && nvHttpClient.httpClient != nil { - nvHttpClient.httpClient.CloseIdleConnections() + if nvHttpClient != nil { + if nvHttpClient.httpClient != nil { + nvHttpClient.httpClient.CloseIdleConnections() + } + if nvHttpClient.insecureHttpClient != nil { + nvHttpClient.insecureHttpClient.CloseIdleConnections() + } } } else { if nvHttpClient.proxyUrlStr != newProxy.URL { @@ -730,14 +755,10 @@ func initHttpClients() { _sysProxyMutex.Unlock() } for _, proxyOption := range []int8{const_no_proxy, const_https_proxy, const_http_proxy} { - httpClient, proxyUrlStr, basicAuth := createHttpClient(proxyOption, clusterAuthTimeout) + nvHttpClient := createNvHttpClient(proxyOption, clusterAuthTimeout) _httpClientMutex.Lock() if _nvHttpClients[proxyOption] == nil { - _nvHttpClients[proxyOption] = &tNvHttpClient{ - httpClient: httpClient, - proxyUrlStr: proxyUrlStr, - basicAuth: basicAuth, - } + _nvHttpClients[proxyOption] = nvHttpClient } _httpClientMutex.Unlock() } @@ -769,12 +790,12 @@ func sendReqToJointCluster(rc share.CLUSRestServerInfo, clusterID, token, method nvHttpClient = _nvHttpClients[proxyOption] _httpClientMutex.RUnlock() if scanRepository { - nvHttpClient.httpClient.Timeout = repoScanLingeringDuration + time.Duration(30*time.Second) + nvHttpClient.insecureHttpClient.Timeout = repoScanLingeringDuration + time.Duration(30*time.Second) } headers, statusCode, data, err = sendReqToJointClusterInternal(nvHttpClient, method, urlStr, token, contentType, tag, txnID, proxyOption, body, gzipped, forward, remoteExport, logError) if scanRepository { - nvHttpClient.httpClient.Timeout = clusterAuthTimeout + nvHttpClient.insecureHttpClient.Timeout = clusterAuthTimeout } if err == nil { _httpClientMutex.Lock() @@ -793,7 +814,7 @@ func sendReqToJointCluster(rc share.CLUSRestServerInfo, clusterID, token, method func sendReqToJointClusterInternal(nvHttpClient *tNvHttpClient, method, urlStr, token, contentType, tag, txnID string, proxyOption int8, body []byte, gzipped, forward, remoteExport, logError bool) (map[string]string, int, []byte, error) { - var httpClient *http.Client = nvHttpClient.httpClient + var httpClient *http.Client = nvHttpClient.insecureHttpClient req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(body)) if err != nil { @@ -882,18 +903,21 @@ func getJointClusterToken(rc *share.CLUSFedJointClusterInfo, clusterID string, u if !refreshToken { return cacher.GetFedJoinedClusterToken(clusterID, login.id, acc) } else { + masterToken, err := jwtGenFedMasterToken(user, login, rc.ID, rc.Secret) + if err != nil { + return "", err + } reqTo := &api.RESTFedAuthData{ ClientIP: _masterClusterIP, MasterUsername: login.fullname, JointUsername: common.DefaultAdminUser, // master token is for requesting regular jwt token from joint cluster. It can be validated by joint cluster based on shared secret/key/cert between master & joint clusters - MasterToken: jwtGenFedMasterToken(user, login, rc.ID, rc.Secret), + MasterToken: masterToken, } if reqTo.MasterToken == "" { return "", common.ErrObjectAccessDenied } - var err error var data []byte var statusCode int var proxyUsed bool @@ -1262,7 +1286,10 @@ func pingJointCluster(tag, urlStr string, jointCluster share.CLUSFedJointCluster FedKvVersion: kv.GetFedKvVer(), } if id != "" { - reqTo.Token = jwtGenFedPingToken(api.FedRoleMaster, id, jointCluster.Secret, nil) + reqTo.Token, err = jwtGenFedPingToken(api.FedRoleMaster, id, jointCluster.Secret, nil) + if err != nil { + return 0, false, err + } } bodyTo, _ := json.Marshal(&reqTo) cmdResp := cmdResponse{id: id, result: _fedClusterDisconnected} @@ -2301,9 +2328,9 @@ func handlerJoinFedInternal(w http.ResponseWriter, r *http.Request, ps httproute // verify if joint cluster is reachable from master cluster var jointCluster share.CLUSFedJointClusterInfo jointCluster.RestInfo = reqData.JointCluster.RestInfo - statusCode, proxyUsed, _ := pingJointCluster(_tagVerifyJointCluster, "v1/fed/joint_test_internal", jointCluster, nil, accReadAll) - if statusCode != http.StatusOK { - log.WithFields(log.Fields{"statusCode": statusCode, "rest": reqData.JointCluster.RestInfo}).Error("Managed cluster unreachable") + statusCode, proxyUsed, err2 := pingJointCluster(_tagVerifyJointCluster, "v1/fed/joint_test_internal", jointCluster, nil, accReadAll) + if statusCode != http.StatusOK || err2 != nil { + log.WithFields(log.Fields{"statusCode": statusCode, "rest": reqData.JointCluster.RestInfo, "error": err2}).Error("Managed cluster unreachable") restRespError(w, http.StatusBadRequest, api.RESTErrFedJointUnreachable) return }
controller/rest/rest.go+20 −18 modified@@ -174,6 +174,7 @@ var restErrMessage = []string{ api.RESTErrRemoteExportFail: "Failed to export to remote repository", api.RESTErrInvalidQueryToken: "Invalid or expired query token", api.RESTErrPollJobNotFoundError: "Job not found in the Job Queue", + api.RESTErrServerError: "Server Error", } func restRespForward(w http.ResponseWriter, r *http.Request, statusCode int, headers map[string]string, data []byte, remoteExport, remoteRegScanTest bool) { @@ -1297,24 +1298,25 @@ func (l restLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) { } type Context struct { - LocalDev *common.LocalDevice - EvQueue cluster.ObjectQueueInterface - AuditQueue cluster.ObjectQueueInterface - Messenger cluster.MessengerInterface - Cacher cache.CacheInterface - Scanner scan.ScanInterface - SearchRegistries string - FedPort uint - RESTPort uint - PwdValidUnit uint - TeleNeuvectorURL string - TeleFreq uint - NvAppFullVersion string - NvSemanticVersion string - CspType share.TCspType - CspPauseInterval uint // in minutes - CustomCheckControl string // disable / strict / loose - CheckCrdSchemaFunc func(lead, init, crossCheck bool, cspType share.TCspType) []string + LocalDev *common.LocalDevice + EvQueue cluster.ObjectQueueInterface + AuditQueue cluster.ObjectQueueInterface + Messenger cluster.MessengerInterface + Cacher cache.CacheInterface + Scanner scan.ScanInterface + SearchRegistries string + FedPort uint + RESTPort uint + PwdValidUnit uint + TeleNeuvectorURL string + TeleSkipTlsVerification bool // default value is false, meaning it is secure by default + TeleFreq uint + NvAppFullVersion string + NvSemanticVersion string + CspType share.TCspType + CspPauseInterval uint // in minutes + CustomCheckControl string // disable / strict / loose + CheckCrdSchemaFunc func(lead, init, crossCheck bool, cspType share.TCspType) []string } var cctx *Context
controller/rest/system.go+39 −20 modified@@ -2324,6 +2324,7 @@ func getNvUpgradeInfo() *api.RESTCheckUpgradeInfo { func getAcceptableAlerts(acc *access.AccessControl, login *loginSession) ([]string, []string, []string, []string, []string, map[string]string, utils.Set) { var clusterRoleErrors, clusterRoleBindingErrors, roleErrors, roleBindingErrors, nvCrdSchemaErrors []string + otherAlerts := map[string]string{} if k8sPlatform { clusterRoleErrors, clusterRoleBindingErrors, roleErrors, roleBindingErrors = resource.VerifyNvK8sRBAC(localDev.Host.Flavor, "", false) @@ -2345,8 +2346,18 @@ func getAcceptableAlerts(acc *access.AccessControl, login *loginSession) ([]stri } acceptedAlerts := utils.NewSetFromStringSlice(accepted) + if k8sPlatform { + if secretResetByNV := resource.IsStoreSecretUpdatedByNV(); secretResetByNV { + alert := "Important: Please backup the data in Kubernetes secret neuvector-store-secret reset by NeuVector" + b := sha256.Sum256([]byte(alert)) + key := hex.EncodeToString(b[:]) + if !acceptedAlerts.Contains(key) { + otherAlerts[key] = alert + } + } + } + fedRole := cacher.GetFedMembershipRoleNoAuth() - otherAlerts := map[string]string{} if (fedRole == api.FedRoleMaster && (acc.IsFedReader() || acc.IsFedAdmin() || acc.HasPermFed())) || (fedRole == api.FedRoleJoint && acc.HasGlobalPermissions(share.PERMS_CLUSTER_READ, 0)) { // _fedClusterLeft(206), _fedClusterDisconnected(204) @@ -2478,9 +2489,14 @@ func handlerSystemGetAlerts(w http.ResponseWriter, r *http.Request, ps httproute if NvCrdSchemaAlertGroup := getAlertGroup(nvCrdSchemaAlerts, api.AlertTypeRBAC, acceptedAlerts); NvCrdSchemaAlertGroup != nil { resp.AcceptableAlerts.NvCrdSchemaAlerts = NvCrdSchemaAlertGroup } + + if NvCrdSchemaAlertGroup := getAlertGroup(nvCrdSchemaAlerts, api.AlertTypeRBAC, acceptedAlerts); NvCrdSchemaAlertGroup != nil { + resp.AcceptableAlerts.NvCrdSchemaAlerts = NvCrdSchemaAlertGroup + } + if otherAlerts != nil { otherAlertGroup := &api.RESTNvAlertGroup{ - Type: api.AlertTypeRBAC, + Type: api.AlertTypeOthers, } for id, msg := range otherAlerts { otherAlertGroup.Data = append(otherAlertGroup.Data, &api.RESTNvAlert{ @@ -2806,6 +2822,10 @@ func _importHandler(w http.ResponseWriter, r *http.Request, tid, importType, tem restRespErrorMessageEx(w, status, api.RESTErrFailImport, importTask.Status, resp) } else { // import is not running and caller tries to query the last import status + resp.Data.FailToDecryptKeyFields = importTask.FailToDecryptKeyFields + if len(importTask.FailToDecryptKeyFields) > 0 { + log.WithFields(log.Fields{"FailToDecryptKeyFields": importTask.FailToDecryptKeyFields}).Warn() + } restRespSuccess(w, r, &resp, acc, login, nil, "") if importType == share.IMPORT_TYPE_CONFIG { if importTask.Status == share.IMPORT_DONE { @@ -2872,16 +2892,6 @@ func _importHandler(w http.ResponseWriter, r *http.Request, tid, importType, tem } if err == nil { var tempToken string - if importType == share.IMPORT_TYPE_CONFIG { - user := &share.CLUSUser{ - Fullname: login.fullname, - Username: login.fullname, - Server: login.server, - } - domainRoles := access.DomainRole{access.AccessDomainGlobal: api.UserRoleImportStatus} - _, tempToken, _ = jwtGenerateToken(user, domainRoles, nil, login.remote, login.mainSessionID, "", nil) - } - importTask.TotalLines = lines importTask.Percentage = 3 importTask.LastUpdateTime = time.Now().UTC() @@ -2890,14 +2900,23 @@ func _importHandler(w http.ResponseWriter, r *http.Request, tid, importType, tem eps := cacher.GetAllControllerRPCEndpoints(access.NewReaderAccessControl()) switch importType { case share.IMPORT_TYPE_CONFIG: - value := r.Header.Get("X-As-Standalone") - ignoreFed, _ := strconv.ParseBool(value) - go func() { - if err := cfgHelper.Import(eps, localDev.Ctrler.ID, localDev.Ctrler.ClusterIP, login.domainRoles, importTask, - tempToken, revertFedRoles, postImportOp, rpc.PauseResumeStoreWatcher, ignoreFed); err != nil { - log.WithFields(log.Fields{"error": err}).Error("Import") - } - }() + user := &share.CLUSUser{ + Fullname: login.fullname, + Username: login.fullname, + Server: login.server, + } + domainRoles := access.DomainRole{access.AccessDomainGlobal: api.UserRoleImportStatus} + _, tempToken, _, err = jwtGenerateToken(user, domainRoles, nil, login.remote, login.mainSessionID, "", nil) + if err == nil { + value := r.Header.Get("X-As-Standalone") + ignoreFed, _ := strconv.ParseBool(value) + go func() { + if err := cfgHelper.Import(eps, localDev.Ctrler.ID, localDev.Ctrler.ClusterIP, login.domainRoles, importTask, + tempToken, revertFedRoles, postImportOp, rpc.PauseResumeStoreWatcher, ignoreFed); err != nil { + log.WithFields(log.Fields{"error": err}).Error("Import") + } + }() + } case share.IMPORT_TYPE_GROUP_POLICY: go func() { if err := importGroupPolicy(share.ScopeLocal, login.domainRoles, importTask, postImportOp); err != nil {
monitor/monitor.c+40 −7 modified@@ -52,16 +52,20 @@ #define ENV_PWD_VALID_UNIT "PWD_VALID_UNIT" #define ENV_RANCHER_EP "RANCHER_EP" #define ENV_RANCHER_SSO "RANCHER_SSO" -#define ENV_TELE_NEUVECTOR_EP "TELEMETRY_NEUVECTOR_EP" -#define ENV_TELE_CURRENT_VER "TELEMETRY_CURRENT_VER" -#define ENV_TELEMETRY_FREQ "TELEMETRY_FREQ" #define ENV_NO_DEFAULT_ADMIN "NO_DEFAULT_ADMIN" #define ENV_CSP_ENV "CSP_ENV" #define ENV_CSP_PAUSE_INTERVAL "CSP_PAUSE_INTERVAL" #define ENV_AUTOPROFILE_CLT "AUTO_PROFILE_COLLECT" #define ENV_SET_CUSTOM_BENCH "CUSTOM_CHECK_CONTROL" #define ENV_SHOW_ALL_CMD "SHOW_ALL_COMMAND" +#define ENV_KEY_ROTATION_PERIOD "KEY_ROTATION_PERIOD" +#define ENV_CHECK_ROTATE_PERIOD "CHECK_KEY_ROTATION_PERIOD" +#define ENV_TELE_NEUVECTOR_EP "TELEMETRY_NEUVECTOR_EP" +#define ENV_TELE_CURRENT_VER "TELEMETRY_CURRENT_VER" +#define ENV_TELEMETRY_FREQ "TELEMETRY_FREQ" +#define ENV_INSECURE_SKIP_TELE_TLS_VERIFY "INSECURE_SKIP_TELEMETRY_TLS_VERIFICATION" + #define ENV_SCANNER_DOCKER_URL "SCANNER_DOCKER_URL" #define ENV_SCANNER_LICENSE "SCANNER_LICENSE" #define ENV_SCANNER_ON_DEMAND "SCANNER_ON_DEMAND" @@ -242,7 +246,7 @@ static pid_t fork_exec(int i) char *registry, *repository, *tag, *user, *pass, *base, *api_user, *api_pass, *enable; char *pwd_valid_unit, *rancher_ep, *debug_level, *policy_pull_period, *search_regs; char *telemetry_neuvector_ep, *telemetry_current_ver, *telemetry_freq, *csp_env, *csp_pause_interval; - char *custom_check_control, *log_level; + char *custom_check_control, *log_level, *key_rotation_period, *check_key_rotation_period; char *max_scanner_tasks, *max_concurrent_repo_scan_tasks, *scanner_lb_max, *scan_job_queue_capacity, *scan_job_fail_retry_max, *repo_scan_long_poll_timeout, *stale_scan_job_cleanup_interval_hour; int a; @@ -449,6 +453,17 @@ static pid_t fork_exec(int i) args[a++] = "-telemetry_freq"; args[a++] = telemetry_freq; } + if ((key_rotation_period = getenv(ENV_KEY_ROTATION_PERIOD)) != NULL) { + args[a++] = "-key_rotation_period"; + args[a++] = key_rotation_period; + } + if ((check_key_rotation_period = getenv(ENV_CHECK_ROTATE_PERIOD)) != NULL) { + args[a++] = "-check_key_rotation_period"; + args[a++] = check_key_rotation_period; + } + if (getenv(ENV_INSECURE_SKIP_TELE_TLS_VERIFY)) { + args[a++] = "-insecure_skip_telemetry_tls_verification"; + } if ((enable = getenv(ENV_NO_DEFAULT_ADMIN)) != NULL) { if (checkImplicitEnableFlag(enable) == 1) { args[a ++] = "-no_def_admin"; @@ -499,16 +514,16 @@ static pid_t fork_exec(int i) if ((stale_scan_job_cleanup_interval_hour = getenv(ENV_STALE_SCAN_JOB_CLEANUP_INTERVAL_HOUR)) != NULL) { args[a++] = "-stale_scan_job_cleanup_interval_hour"; args[a++] = stale_scan_job_cleanup_interval_hour; - } + } if ((repo_scan_long_poll_timeout = getenv(ENV_REPO_SCAN_LONG_POLL_TIMEOUT)) != NULL) { args[a++] = "-repo_scan_long_poll_timeout"; args[a++] = repo_scan_long_poll_timeout; - } + } if ((scanner_lb_max = getenv(ENV_SCANNER_LB_MAX)) != NULL) { args[a++] = "-scanner_lb_max"; args[a++] = scanner_lb_max; } - + // debug("Start %s, pid=%d\n", g_procs[i].name, g_procs[i].pid); args[a] = NULL; break; @@ -681,6 +696,16 @@ static void stop_proc(int i, int sig, int wait) #define DEFAULT_RPC_PORT "18300" #define DEFAULT_LAN_PORT "18301" +static bool is_valid_port(const char *strPort) { + if ((strPort == NULL) || (strlen(strPort) != strspn(strPort, "0123456789"))) { + return false; // Handle NULL string and empty string case + } + + int port = atoi(strPort); + + return ((port > 0) && (port <= 65535)); +} + static int check_consul_ports(void) { FILE *fp; @@ -698,6 +723,14 @@ static int check_consul_ports(void) if (lan_port == NULL) { lan_port = DEFAULT_LAN_PORT; } + if (!is_valid_port(rpc_port)) { + debug("invalid consul rpc port %s\n", rpc_port); + return -1; + } + if (!is_valid_port(lan_port)) { + debug("invalid consul lan port %s\n", lan_port); + return -1; + } sprintf(shbuf,"ss -lnp|grep '%s\\|%s'",rpc_port, lan_port); fp = popen(shbuf, "r");
share/clus_apis.go+26 −13 modified@@ -39,6 +39,7 @@ const CLUSLockFedScanDataKey string = CLUSLockStore + "fed_scan_data" const CLUSLockApikeyKey string = CLUSLockStore + "apikey" const CLUSLockVulnKey string = CLUSLockStore + "vulnerability" const CLUSLockCompKey string = CLUSLockStore + "compliance" +const CLUSStoreSecretKey string = CLUSLockStore + "store_secret" //const CLUSLockResponseRuleKey string = CLUSLockStore + "response_rule" @@ -163,6 +164,8 @@ const CLUSNodeCommonProfileStore string = CLUSNodeCommonStoreKey + CLUSWorkloadP // state const CLUSCtrlEnabledValue string = "ok" +const CLUSSystemEncMigratedKey string = CLUSStateStore + "enc_migrated" + // cluster key represent one installation, which will remain unchanged when controllers // come and go, and rolling upgrade. It is not part of system configuration. const CLUSCtrlInstallationKey string = CLUSStateStore + "installation" @@ -174,7 +177,7 @@ const CLUSCtrlVerKey string = CLUSStateStore + "ctrl_ver" const CLUSKvRestoreKey string = CLUSStateStore + "kv_restore" const CLUSExpiredTokenStore string = CLUSStateStore + "expired_token/" const CLUSImportStore string = CLUSStateStore + "import/" - +const CLUSNextKeyRotationTSKey string = CLUSStateStore + "next_key_rotation_ts" const CLUSConfigSecretPatternsKey string = CLUSConfigCustomRuleStore + "secret_patterns" func CLUSExpiredTokenKey(token string) string { @@ -834,6 +837,11 @@ type CLUSSystemConfig struct { AllowNsUserExportNetPolicy bool `json:"allow_ns_user_export_net_policy,omitempty"` } +type CLUSSystemConfigEncMigrated struct { + EncMigratedSystemConfig string `json:"enc_migrated_system_config"` + EncDataExpirationTime time.Time `json:"enc_data_expiration_time"` +} + type CLUSSystemConfigAutoscale struct { Strategy string `json:"strategy"` MinPods uint32 `json:"min_pods"` @@ -1478,6 +1486,10 @@ const ( CLUSEvGroupMetricViolation //network metric violation per group level CLUSEvKvRestored // kv is restored from pvc CLUSEvScanDataRestored // scan data is restored from pvc + CLUSEvMismatchedDEKSeed // mismatched dekSeed for backup from pvc + CLUSEvDEKSeedUnavailable // dekSeed unavailable (most likely because of RBAC neuvector-binding-secret-controller) + CLUSEvReEncryptWithDEK // re-encrypt sensitive data in backup files with variant DEK + CLUSEvEncryptionSecretSet // neuvector-store-secret secret is set ) const ( @@ -2984,18 +2996,19 @@ func CLUSImportOpKey(name string) string { } type CLUSImportTask struct { - TID string `json:"tid"` - ImportType string `json:"import_type"` - CtrlerID string `json:"ctrler_id"` - TempFilename string `json:"temp_filename"` - Status string `json:"status"` - Percentage int `json:"percentage"` - TotalLines int `json:"total_lines"` - LastUpdateTime time.Time `json:"last_update_time"` - CallerFullname string `json:"caller_fullname"` - CallerRemote string `json:"caller_remote"` - CallerID string `json:"caller_id"` - Overwrite string `json:"overwrite"` + TID string `json:"tid"` + ImportType string `json:"import_type"` + CtrlerID string `json:"ctrler_id"` + TempFilename string `json:"temp_filename"` + Status string `json:"status"` + Percentage int `json:"percentage"` + TotalLines int `json:"total_lines"` + LastUpdateTime time.Time `json:"last_update_time"` + CallerFullname string `json:"caller_fullname"` + CallerRemote string `json:"caller_remote"` + CallerID string `json:"caller_id"` + Overwrite string `json:"overwrite"` + FailToDecryptKeyFields map[string][]string `json:"fail_to_decrypt_key_fields"` // key_path : []fields } func CLUSNodeProfileStoreKey(nodeID string) string {
share/utils/utils.go+13 −19 modified@@ -1058,34 +1058,28 @@ func EncryptSensitive(data string, key []byte) string { return encrypted } -func DecryptUserToken(encrypted string, key []byte) string { +func DecryptUserToken(encrypted string, key []byte) (string, error) { + if len(key) == 0 { + return "", errors.New("empty encryption key") + } if encrypted == "" { - return "" + return "", nil } - encrypted = strings.ReplaceAll(encrypted, "_", "/") - if key == nil { - key = getPasswordSymKey() - } - token, _ := DecryptFromRawStdBase64(key, encrypted) - return token + return DecryptFromRawURLBase64(key, encrypted) } -// User token cannot have / in it and cannot have - as the first char. -func EncryptUserToken(token string, key []byte) string { - if token == "" { - return "" +func EncryptUserToken(token string, key []byte) (string, error) { + if len(key) == 0 { + return "", errors.New("empty encryption key") } - - if key == nil { - key = getPasswordSymKey() + if token == "" { + return "", nil } // Std base64 encoding has + and /, instead of - and _ (url encoding) - // token can be part of kv key, so we replace / with _ - encrypted, _ := EncryptToRawStdBase64(key, []byte(token)) - encrypted = strings.ReplaceAll(encrypted, "/", "_") - return encrypted + // encrypted token can be used as part of kv key string, so we replace / with _ + return EncryptToRawURLBase64(key, []byte(token)) } func DecryptURLSafe(encrypted string) string {
share/utils/utils_test.go+16 −5 modified@@ -184,12 +184,23 @@ func TestPlatformEnv(t *testing.T) { } } -func TestBase64Encrypt(t *testing.T) { +func TestUserTokenEncrypt(t *testing.T) { token := "123456" - encrypt := EncryptUserToken(token, nil) - decrypt := DecryptUserToken(encrypt, nil) - if decrypt != token { - t.Errorf("Token encrypt error: token=%v decrypt=%v\n", token, decrypt) + encrypt, err := EncryptUserToken(token, nil) + if encrypt != "" { + t.Errorf("Token encrypt unexpected: token=%v encrypt=%v err=%v\n", token, encrypt, err) + } + key, err := GetGuid() + if err != nil { + t.Errorf("failed to call GetGuid: %v", err) + } + encrypt, err = EncryptUserToken(token, []byte(key)) + if err != nil { + t.Errorf("Token encrypt unexpected: token=%v encrypt=%v err=%v\n", token, encrypt, err) + } + decrypt, err := DecryptUserToken(encrypt, []byte(key)) + if decrypt != token || err != nil { + t.Errorf("Token encrypt error: token=%v encrypt=%v decrypt=%v err=%v\n", token, encrypt, decrypt, err) } }
415737cbec58NVSHAS-10117: backport the changes for NVSHAS-9979-9987 to 5.3.5
4 files changed · +138 −89
controller/controller.go+19 −17 modified@@ -249,6 +249,7 @@ func main() { pwdValidUnit := flag.Uint("pwd_valid_unit", 1440, "") rancherEP := flag.String("rancher_ep", "", "Rancher endpoint URL") rancherSSO := flag.Bool("rancher_sso", false, "Rancher SSO integration") + insecureSkipTeleTLSVerification := flag.Bool("insecure_skip_telemetry_tls_verification", false, "Do not verify Telemetry server's certificate chain and host name") teleNeuvectorEP := flag.String("telemetry_neuvector_ep", "", "") // for testing only teleCurrentVer := flag.String("telemetry_current_ver", "", "") // in the format {major}.{minor}.{patch}[-s{#}], for testing only telemetryFreq := flag.Uint("telemetry_freq", 60, "") // in minutes, for testing only @@ -752,23 +753,24 @@ func main() { opa.InitOpaServer() rctx := rest.Context{ - LocalDev: dev, - EvQueue: evqueue, - AuditQueue: auditQueue, - Messenger: messenger, - Cacher: cacher, - Scanner: scanner, - RESTPort: *restPort, - FedPort: *fedPort, - PwdValidUnit: *pwdValidUnit, - TeleNeuvectorURL: *teleNeuvectorEP, - TeleFreq: *telemetryFreq, - NvAppFullVersion: nvAppFullVersion, - NvSemanticVersion: nvSemanticVersion, - CspType: cspType, - CspPauseInterval: *cspPauseInterval, - CustomCheckControl: *custom_check_control, - CheckCrdSchemaFunc: nvcrd.CheckCrdSchema, + LocalDev: dev, + EvQueue: evqueue, + AuditQueue: auditQueue, + Messenger: messenger, + Cacher: cacher, + Scanner: scanner, + RESTPort: *restPort, + FedPort: *fedPort, + PwdValidUnit: *pwdValidUnit, + TeleNeuvectorURL: *teleNeuvectorEP, + TeleSkipTlsVerification: *insecureSkipTeleTLSVerification, + TeleFreq: *telemetryFreq, + NvAppFullVersion: nvAppFullVersion, + NvSemanticVersion: nvSemanticVersion, + CspType: cspType, + CspPauseInterval: *cspPauseInterval, + CustomCheckControl: *custom_check_control, + CheckCrdSchemaFunc: nvcrd.CheckCrdSchema, } // rest.PreInitContext() must be called before orch connector because existing CRD handling could happen right after orch connecter starts rest.PreInitContext(&rctx)
controller/rest/federation.go+74 −50 modified@@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "mime" "net/http" @@ -47,9 +48,10 @@ type cmdResponse struct { } type tNvHttpClient struct { - httpClient *http.Client - proxyUrlStr string // non-empty for connection thru proxy - basicAuth string // non-empty for proxy that requires auth + httpClient *http.Client // HTTP client with TLS verification enabled. + insecureHttpClient *http.Client // HTTP client with TLS verification skipped. + proxyUrlStr string // non-empty for connection thru proxy + basicAuth string // non-empty for proxy that requires auth } const ( @@ -410,21 +412,40 @@ func getProxyURL(r *http.Request) (*url.URL, error) { return nil, nil } -func createHttpClient(proxyOption int8, timeout time.Duration) (*http.Client, string, string) { - var proxyUrlStr string - var basicAuth string - var proxy share.CLUSProxy - +func createHttpClient(basicAuth string, timeout time.Duration, tlsInsecureSkipVerify bool) *http.Client { // refer to http.DefaultTransport transport := &http.Transport{ Proxy: getProxyURL, TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: tlsInsecureSkipVerify, }, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, DisableCompression: true, } + if basicAuth != "" { + transport.ProxyConnectHeader = http.Header{} + transport.ProxyConnectHeader.Add("Proxy-Authorization", basicAuth) + } + httpClient := &http.Client{ + Transport: transport, + Timeout: timeout, + } + jar, err := cookiejar.New(nil) + if err != nil { + log.WithFields(log.Fields{"tlsInsecureSkipVerify": tlsInsecureSkipVerify, "err": err}).Error("creating cookie jar") + } else { + httpClient.Jar = jar + } + + return httpClient +} + +func createNvHttpClient(proxyOption int8, timeout time.Duration) *tNvHttpClient { + var proxyUrlStr string + var basicAuth string + var proxy share.CLUSProxy + if proxyOption != const_no_proxy { _sysProxyMutex.RLock() if proxyOption == const_http_proxy { @@ -439,22 +460,15 @@ func createHttpClient(proxyOption int8, timeout time.Duration) (*http.Client, st if proxy.Username != "" { auth := fmt.Sprintf("%s:%s", proxy.Username, proxy.Password) basicAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) - transport.ProxyConnectHeader = http.Header{} - transport.ProxyConnectHeader.Add("Proxy-Authorization", basicAuth) } } - httpClient := &http.Client{ - Transport: transport, - Timeout: timeout, - } - jar, err := cookiejar.New(nil) - if err != nil { - log.WithFields(log.Fields{"proxyOption": proxyOption, "err": err}).Error("creating cookie jar") - } else { - httpClient.Jar = jar - } - return httpClient, proxyUrlStr, basicAuth + return &tNvHttpClient{ + httpClient: createHttpClient(basicAuth, timeout, false), // http.Transport.TLSClientConfig.InsecureSkipVerify is false + insecureHttpClient: createHttpClient(basicAuth, timeout, true), // http.Transport.TLSClientConfig.InsecureSkipVerify is true + proxyUrlStr: proxyUrlStr, + basicAuth: basicAuth, + } } func getProxyOptions(id string, useProxy int8) []int8 { @@ -536,17 +550,12 @@ func sendRestRequest(idTarget string, method, urlStr, token, cntType, jointTicke nvHttpClient = _nvHttpClients[proxyOption] _httpClientMutex.RUnlock() if nvHttpClient == nil { - httpClient, proxyUrlStr, basicAuth := createHttpClient(proxyOption, clusterAuthTimeout) - nvHttpClient = &tNvHttpClient{ - httpClient: httpClient, - proxyUrlStr: proxyUrlStr, - basicAuth: basicAuth, - } + nvHttpClient = createNvHttpClient(proxyOption, clusterAuthTimeout) _httpClientMutex.Lock() _nvHttpClients[proxyOption] = nvHttpClient _httpClientMutex.Unlock() } - if data, statusCode, err = sendRestReqInternal(nvHttpClient, method, urlStr, token, cntType, + if data, statusCode, err = sendRestReqInternal(nvHttpClient, idTarget, method, urlStr, token, cntType, jointTicket, jointID, proxyOption, cookie, body, logError); err == nil { _httpClientMutex.Lock() @@ -562,14 +571,17 @@ func sendRestRequest(idTarget string, method, urlStr, token, cntType, jointTicke return data, statusCode, usedProxy, err } -func sendRestReqInternal(nvHttpClient *tNvHttpClient, method, urlStr, token, cntType, jointTicket, jointID string, +func sendRestReqInternal(nvHttpClient *tNvHttpClient, idTarget, method, urlStr, token, cntType, jointTicket, jointID string, proxyOption int8, cookie *http.Cookie, body []byte, logError bool) ([]byte, int, error) { - - var httpClient *http.Client = nvHttpClient.httpClient + var httpClient *http.Client = nvHttpClient.insecureHttpClient var req *http.Request var gzipped bool var err error + if idTarget == "telemetry" && !cctx.TeleSkipTlsVerification { + httpClient = nvHttpClient.httpClient + } + if jointTicket != "" && jointID != "" { if len(body) > gzipThreshold { body = utils.GzipBytes(body) @@ -632,7 +644,23 @@ func sendRestReqInternal(nvHttpClient *tNvHttpClient, method, urlStr, token, cnt } defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) + var data []byte + if idTarget == "telemetry" { + // because neuvector_versions.json only has one version entry, the expected len of returned neuvector_versions.json should be < 256 + var dataBuffer [256]byte + reader := io.LimitReader(resp.Body, int64(len(dataBuffer))) + n, err2 := reader.Read(dataBuffer[:]) + if err2 != io.EOF { + err = fmt.Errorf("unexpected %d byes of data read, error: %v", n, err2) + if _, err2 = io.Copy(io.Discard, resp.Body); err2 != nil { + log.WithFields(log.Fields{"error": err2}).Error("Discard remaining data fail") + } + } else { + data = dataBuffer[:n] + } + } else { + data, err = io.ReadAll(resp.Body) + } if err != nil { log.WithFields(log.Fields{"url": urlStr, "status": resp.Status, "proxyOption": proxyOption}).Error("Read data fail") return nil, 0, err @@ -692,17 +720,17 @@ func RestConfig(cmd, interval uint32, param1 interface{}, param2 interface{}) er // no need to reconstruct a new http client when only proxy url changes. // however, if proxy auth changes, we need to reconstruct a new http client because transport ProxyConnectHeader is a map if nvHttpClient == nil || newBasicAuth != nvHttpClient.basicAuth { - newHttpClient, proxyUrlStr, basicAuth := createHttpClient(proxyOption, clusterAuthTimeout) - newNvHttpClient := &tNvHttpClient{ - httpClient: newHttpClient, - proxyUrlStr: proxyUrlStr, - basicAuth: basicAuth, - } + newNvHttpClient := createNvHttpClient(proxyOption, clusterAuthTimeout) _httpClientMutex.Lock() _nvHttpClients[proxyOption] = newNvHttpClient _httpClientMutex.Unlock() - if nvHttpClient != nil && nvHttpClient.httpClient != nil { - nvHttpClient.httpClient.CloseIdleConnections() + if nvHttpClient != nil { + if nvHttpClient.httpClient != nil { + nvHttpClient.httpClient.CloseIdleConnections() + } + if nvHttpClient.insecureHttpClient != nil { + nvHttpClient.insecureHttpClient.CloseIdleConnections() + } } } else { if nvHttpClient.proxyUrlStr != newProxy.URL { @@ -726,14 +754,10 @@ func initHttpClients() { _sysProxyMutex.Unlock() } for _, proxyOption := range []int8{const_no_proxy, const_https_proxy, const_http_proxy} { - httpClient, proxyUrlStr, basicAuth := createHttpClient(proxyOption, clusterAuthTimeout) + nvHttpClient := createNvHttpClient(proxyOption, clusterAuthTimeout) _httpClientMutex.Lock() if _nvHttpClients[proxyOption] == nil { - _nvHttpClients[proxyOption] = &tNvHttpClient{ - httpClient: httpClient, - proxyUrlStr: proxyUrlStr, - basicAuth: basicAuth, - } + _nvHttpClients[proxyOption] = nvHttpClient } _httpClientMutex.Unlock() } @@ -765,12 +789,12 @@ func sendReqToJointCluster(rc share.CLUSRestServerInfo, clusterID, token, method nvHttpClient = _nvHttpClients[proxyOption] _httpClientMutex.RUnlock() if scanRepository { - nvHttpClient.httpClient.Timeout = repoScanLingeringDuration + time.Duration(30*time.Second) + nvHttpClient.insecureHttpClient.Timeout = repoScanLingeringDuration + time.Duration(30*time.Second) } headers, statusCode, data, err = sendReqToJointClusterInternal(nvHttpClient, method, urlStr, token, contentType, tag, txnID, proxyOption, body, gzipped, forward, remoteExport, logError) if scanRepository { - nvHttpClient.httpClient.Timeout = clusterAuthTimeout + nvHttpClient.insecureHttpClient.Timeout = clusterAuthTimeout } if err == nil { _httpClientMutex.Lock() @@ -789,7 +813,7 @@ func sendReqToJointCluster(rc share.CLUSRestServerInfo, clusterID, token, method func sendReqToJointClusterInternal(nvHttpClient *tNvHttpClient, method, urlStr, token, contentType, tag, txnID string, proxyOption int8, body []byte, gzipped, forward, remoteExport, logError bool) (map[string]string, int, []byte, error) { - var httpClient *http.Client = nvHttpClient.httpClient + var httpClient *http.Client = nvHttpClient.insecureHttpClient req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(body)) if err != nil {
controller/rest/rest.go+18 −18 modified@@ -1250,24 +1250,24 @@ func (l restLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) { } type Context struct { - LocalDev *common.LocalDevice - EvQueue cluster.ObjectQueueInterface - AuditQueue cluster.ObjectQueueInterface - Messenger cluster.MessengerInterface - Cacher cache.CacheInterface - Scanner scan.ScanInterface - FedPort uint - RESTPort uint - PwdValidUnit uint - TeleNeuvectorURL string - TeleFreq uint - NvAppFullVersion string - NvSemanticVersion string - CspType share.TCspType - CspPauseInterval uint // in minutes - CustomCheckControl string // disable / strict / loose - CheckCrdSchemaFunc func(lead, init, crossCheck bool, cspType share.TCspType) []string - CertManager *kv.CertManager + LocalDev *common.LocalDevice + EvQueue cluster.ObjectQueueInterface + AuditQueue cluster.ObjectQueueInterface + Messenger cluster.MessengerInterface + Cacher cache.CacheInterface + Scanner scan.ScanInterface + FedPort uint + RESTPort uint + PwdValidUnit uint + TeleNeuvectorURL string + TeleSkipTlsVerification bool // default value is false, meaning it is secure by default + TeleFreq uint + NvAppFullVersion string + NvSemanticVersion string + CspType share.TCspType + CspPauseInterval uint // in minutes + CustomCheckControl string // disable / strict / loose + CheckCrdSchemaFunc func(lead, init, crossCheck bool, cspType share.TCspType) []string } var cctx *Context
monitor/monitor.c+27 −4 modified@@ -50,15 +50,17 @@ #define ENV_PWD_VALID_UNIT "PWD_VALID_UNIT" #define ENV_RANCHER_EP "RANCHER_EP" #define ENV_RANCHER_SSO "RANCHER_SSO" -#define ENV_TELE_NEUVECTOR_EP "TELEMETRY_NEUVECTOR_EP" -#define ENV_TELE_CURRENT_VER "TELEMETRY_CURRENT_VER" -#define ENV_TELEMETRY_FREQ "TELEMETRY_FREQ" #define ENV_NO_DEFAULT_ADMIN "NO_DEFAULT_ADMIN" #define ENV_CSP_ENV "CSP_ENV" #define ENV_CSP_PAUSE_INTERVAL "CSP_PAUSE_INTERVAL" #define ENV_AUTOPROFILE_CLT "AUTO_PROFILE_COLLECT" #define ENV_SET_CUSTOM_BENCH "CUSTOM_CHECK_CONTROL" +#define ENV_TELE_NEUVECTOR_EP "TELEMETRY_NEUVECTOR_EP" +#define ENV_TELE_CURRENT_VER "TELEMETRY_CURRENT_VER" +#define ENV_TELEMETRY_FREQ "TELEMETRY_FREQ" +#define ENV_INSECURE_SKIP_TELE_TLS_VERIFY "INSECURE_SKIP_TELEMETRY_TLS_VERIFICATION" + #define ENV_SCANNER_DOCKER_URL "SCANNER_DOCKER_URL" #define ENV_SCANNER_LICENSE "SCANNER_LICENSE" #define ENV_SCANNER_ON_DEMAND "SCANNER_ON_DEMAND" @@ -411,6 +413,9 @@ static pid_t fork_exec(int i) args[a++] = "-telemetry_freq"; args[a++] = telemetry_freq; } + if (getenv(ENV_INSECURE_SKIP_TELE_TLS_VERIFY)) { + args[a++] = "-insecure_skip_telemetry_tls_verification"; + } if ((enable = getenv(ENV_NO_DEFAULT_ADMIN)) != NULL) { if (checkImplicitEnableFlag(enable) == 1) { args[a ++] = "-no_def_admin"; @@ -606,6 +611,16 @@ static void stop_proc(int i, int sig, int wait) #define DEFAULT_RPC_PORT "18300" #define DEFAULT_LAN_PORT "18301" +static bool is_valid_port(const char *strPort) { + if ((strPort == NULL) || (strlen(strPort) != strspn(strPort, "0123456789"))) { + return false; // Handle NULL string and empty string case + } + + int port = atoi(strPort); + + return ((port > 0) && (port <= 65535)); +} + static int check_consul_ports(void) { FILE *fp; @@ -623,7 +638,15 @@ static int check_consul_ports(void) if (lan_port == NULL) { lan_port = DEFAULT_LAN_PORT; } - sprintf(shbuf,"netstat -lnp|grep '%s\\|%s'",rpc_port, lan_port); + if (!is_valid_port(rpc_port)) { + debug("invalid consul rpc port %s\n", rpc_port); + return -1; + } + if (!is_valid_port(lan_port)) { + debug("invalid consul lan port %s\n", lan_port); + return -1; + } + sprintf(shbuf,"ss -lnp|grep '%s\\|%s'",rpc_port, lan_port); fp = popen(shbuf, "r"); if (fp == NULL) {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-qqj3-g7mx-5p4wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-54470ghsaADVISORY
- bugzilla.suse.com/show_bug.cginvdWEB
- github.com/neuvector/neuvector/commit/06424701e69bf1eb76ff90180d78853fded93021ghsaWEB
- github.com/neuvector/neuvector/commit/415737cbec581a5dc5f204fac1c78b7f29ad7dc2ghsaWEB
- github.com/neuvector/neuvector/security/advisories/GHSA-qqj3-g7mx-5p4wnvdWEB
News mentions
0No linked articles in our index yet.