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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/v1alpha1/warpgateconnection_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ type WarpgateConnectionSpec struct {
// insecureSkipVerify disables TLS certificate verification. Not recommended for production.
// +optional
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`

// caSecretRef references a Secret key holding a PEM CA bundle used to verify the
// Warpgate server certificate instead of the system trust store. The key defaults
// to "ca.crt". Ignored when insecureSkipVerify is true.
// +optional
CASecretRef *SecretKeyRef `json:"caSecretRef,omitempty"`
}

// WarpgateConnectionStatus defines the observed state of WarpgateConnection.
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/warpgateconnection_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,11 @@ func validateConnection(conn *WarpgateConnection) (admission.Warnings, error) {
if conn.Spec.AuthSecretRef.Name == "" {
return nil, fmt.Errorf("spec.authSecretRef.name must not be empty")
}
if conn.Spec.CASecretRef != nil && conn.Spec.CASecretRef.Name == "" {
return nil, fmt.Errorf("spec.caSecretRef.name must not be empty")
}
if conn.Spec.CASecretRef != nil && conn.Spec.InsecureSkipVerify {
return admission.Warnings{"caSecretRef is ignored because insecureSkipVerify is true"}, nil
}
return nil, nil
}
30 changes: 30 additions & 0 deletions api/v1alpha1/warpgateconnection_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,36 @@ func TestWarpgateConnectionValidator(t *testing.T) {
}
})

t.Run("caSecretRef accepted", func(t *testing.T) {
c := validConn()
c.Spec.CASecretRef = &SecretKeyRef{Name: "warpgate-ca"}
warnings, err := v.ValidateCreate(ctx, c)
if err != nil || len(warnings) != 0 {
t.Errorf("expected no error or warnings, got %v / %v", err, warnings)
}
})

t.Run("caSecretRef without name rejected", func(t *testing.T) {
c := validConn()
c.Spec.CASecretRef = &SecretKeyRef{Key: "ca.crt"}
if _, err := v.ValidateCreate(ctx, c); err == nil {
t.Error("expected error for empty caSecretRef.name")
}
})

t.Run("caSecretRef with insecureSkipVerify warns", func(t *testing.T) {
c := validConn()
c.Spec.CASecretRef = &SecretKeyRef{Name: "warpgate-ca"}
c.Spec.InsecureSkipVerify = true
warnings, err := v.ValidateCreate(ctx, c)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(warnings) == 0 {
t.Error("expected a warning that caSecretRef is ignored")
}
})

t.Run("update validation works", func(t *testing.T) {
old := validConn()
bad := validConn()
Expand Down
16 changes: 16 additions & 0 deletions api/v1alpha1/warpgateinstance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,16 @@ type WarpgateInstanceSpec struct {

// databaseURL overrides the default SQLite database with a PostgreSQL connection string.
// Example: "postgres://user:pass@host:5432/warpgate"
// This field is deprecated because the value (including any password) is stored in
// plain text in this resource and in the generated Deployment. Use databaseURLSecretRef.
// +optional
DatabaseURL string `json:"databaseURL,omitempty"`

// databaseURLSecretRef references a Secret key holding the database connection string.
// Takes precedence over databaseURL. The key defaults to "url".
// +optional
DatabaseURLSecretRef *SecretKeyRef `json:"databaseURLSecretRef,omitempty"`

// recordSessions enables session recording. Defaults to false.
// +optional
RecordSessions *bool `json:"recordSessions,omitempty"`
Expand Down Expand Up @@ -196,6 +203,15 @@ type InstanceTLSSpec struct {
// If set, cert-manager is not used and the secret is mounted directly.
// +optional
SecretName string `json:"secretName,omitempty"`

// verifyConnection controls TLS verification on the auto-created WarpgateConnection.
// Unset: verify against the CA in secretName (its ca.crt, or tls.crt when there is no
// ca.crt) when secretName is set, and skip verification for the self-signed certificate
// generated inside the pod, which the operator has no copy of.
// true: always verify; reconciliation fails if the operator has no CA to verify against.
// false: never verify.
// +optional
VerifyConnection *bool `json:"verifyConnection,omitempty"`
}

// CertIssuerRef references a cert-manager Issuer or ClusterIssuer.
Expand Down
32 changes: 29 additions & 3 deletions api/v1alpha1/warpgateinstance_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,28 @@ func validatePort(port *int32, fldPath *field.Path, allowZero bool) *field.Error
return nil
}

// validateDatabase checks the external database settings and warns about the
// plain-text databaseURL field.
func validateDatabase(inst *WarpgateInstance, specPath *field.Path) (field.ErrorList, admission.Warnings) {
var errs field.ErrorList
var warnings admission.Warnings
dbRef := inst.Spec.DatabaseURLSecretRef
if dbRef != nil && dbRef.Name == "" {
errs = append(errs, field.Required(specPath.Child("databaseURLSecretRef", "name"), "databaseURLSecretRef.name must not be empty"))
}
if inst.Spec.DatabaseURL != "" || dbRef != nil {
warnings = append(warnings, "an external database is configured; SQLite persistence via PVC is not needed")
}
if inst.Spec.DatabaseURL != "" {
if dbRef != nil {
warnings = append(warnings, "databaseURL is ignored because databaseURLSecretRef is set")
} else {
warnings = append(warnings, "databaseURL is deprecated and stored in plain text; move it to a Secret and use databaseURLSecretRef")
}
}
return errs, warnings
}

// validateWarpgateInstance runs all field-level validation checks.
func validateWarpgateInstance(inst *WarpgateInstance) (admission.Warnings, error) {
var allErrs field.ErrorList
Expand Down Expand Up @@ -253,11 +275,15 @@ func validateWarpgateInstance(inst *WarpgateInstance) (admission.Warnings, error
}
}

if inst.Spec.DatabaseURL != "" {
warnings = append(warnings, "databaseURL is set — SQLite persistence via PVC is not needed")
if tls := inst.Spec.TLS; tls != nil && tls.VerifyConnection != nil && *tls.VerifyConnection && tls.SecretName == "" {
warnings = append(warnings, "tls.verifyConnection is true but tls.secretName is not set; the operator has no CA to verify the auto-created connection against")
}
dbErrs, dbWarnings := validateDatabase(inst, specPath)
allErrs = append(allErrs, dbErrs...)
warnings = append(warnings, dbWarnings...)
externalDB := inst.Spec.DatabaseURL != "" || inst.Spec.DatabaseURLSecretRef != nil
noStorage := inst.Spec.Storage != nil && inst.Spec.Storage.Enabled != nil && !*inst.Spec.Storage.Enabled
if noStorage && inst.Spec.DatabaseURL == "" {
if noStorage && !externalDB {
warnings = append(warnings, "storage is disabled and no databaseURL is set — data will be lost on pod restart")
}

Expand Down
89 changes: 89 additions & 0 deletions api/v1alpha1/warpgateinstance_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,92 @@ func TestInstanceValidate_NoWarnStorageDisabledWithDB(t *testing.T) {
}
}
}

func TestInstanceValidate_WarnDatabaseURLDeprecated(t *testing.T) {
inst := validInstance()
inst.Spec.DatabaseURL = "postgres://user:pass@host:5432/warpgate"

warnings, err := (&WarpgateInstanceCustomValidator{}).ValidateCreate(context.Background(), inst)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !containsWarning(warnings, "deprecated") {
t.Errorf("expected a deprecation warning for databaseURL, got: %v", warnings)
}
}

func TestInstanceValidate_DatabaseURLSecretRef(t *testing.T) {
inst := validInstance()
inst.Spec.DatabaseURLSecretRef = &SecretKeyRef{Name: "wg-db"}

warnings, err := (&WarpgateInstanceCustomValidator{}).ValidateCreate(context.Background(), inst)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if containsWarning(warnings, "deprecated") {
t.Errorf("did not expect a deprecation warning, got: %v", warnings)
}
if !containsWarning(warnings, "SQLite") {
t.Errorf("expected the external database warning, got: %v", warnings)
}

inst.Spec.DatabaseURL = "postgres://inline"
warnings, err = (&WarpgateInstanceCustomValidator{}).ValidateCreate(context.Background(), inst)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !containsWarning(warnings, "ignored") {
t.Errorf("expected a precedence warning when both are set, got: %v", warnings)
}
}

func TestInstanceValidate_DatabaseURLSecretRefRequiresName(t *testing.T) {
inst := validInstance()
inst.Spec.DatabaseURLSecretRef = &SecretKeyRef{Key: "url"}

if _, err := (&WarpgateInstanceCustomValidator{}).ValidateCreate(context.Background(), inst); err == nil {
t.Fatal("expected an error for databaseURLSecretRef without a name")
}
}

func TestInstanceValidate_NoDataLossWarnWithDatabaseURLSecretRef(t *testing.T) {
inst := validInstance()
inst.Spec.Storage = &StorageSpec{Enabled: boolPtr(false), Size: "1Gi"}
inst.Spec.DatabaseURLSecretRef = &SecretKeyRef{Name: "wg-db"}

warnings, err := (&WarpgateInstanceCustomValidator{}).ValidateCreate(context.Background(), inst)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if containsWarning(warnings, "data will be lost") {
t.Errorf("should not warn about data loss with an external database, got: %v", warnings)
}
}

func containsWarning(warnings []string, substr string) bool {
for _, w := range warnings {
if strings.Contains(w, substr) {
return true
}
}
return false
}

func TestInstanceValidate_WarnVerifyConnectionWithoutSecret(t *testing.T) {
inst := validInstance()
inst.Spec.TLS = &InstanceTLSSpec{VerifyConnection: boolPtr(true)}

warnings, err := (&WarpgateInstanceCustomValidator{}).ValidateCreate(context.Background(), inst)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !containsWarning(warnings, "verifyConnection") {
t.Errorf("expected a verifyConnection warning, got: %v", warnings)
}

inst.Spec.TLS.SecretName = "warpgate-tls"
warnings, _ = (&WarpgateInstanceCustomValidator{}).ValidateCreate(context.Background(), inst)
if containsWarning(warnings, "verifyConnection") {
t.Errorf("did not expect a verifyConnection warning with secretName set, got: %v", warnings)
}
}
6 changes: 6 additions & 0 deletions api/v1alpha1/warpgatepasswordcredential_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ type WarpgatePasswordCredentialStatus struct {
UserID string `json:"userID,omitempty"`
// credentialID is the Warpgate-assigned credential UUID.
CredentialID string `json:"credentialID,omitempty"`

// appliedSecretVersion identifies the password Secret revision
// (<name>/<key>@<resourceVersion>) last pushed to Warpgate. When the Secret
// changes, the credential is replaced so the old password stops working.
// +optional
AppliedSecretVersion string `json:"appliedSecretVersion,omitempty"`
// conditions represent the current state.
// +listType=map
// +listMapKey=type
Expand Down
7 changes: 5 additions & 2 deletions api/v1alpha1/warpgatetarget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ type TLSConfigSpec struct {
// mode is the TLS mode: Disabled, Preferred, or Required.
// +kubebuilder:validation:Enum=Disabled;Preferred;Required
Mode string `json:"mode"`
// verify enables TLS certificate verification.
Verify bool `json:"verify,omitempty"`
// verify enables TLS certificate verification. Defaults to true; set it to
// false explicitly to accept unverified certificates.
// +optional
// +kubebuilder:default=true
Verify *bool `json:"verify,omitempty"`
}

// SSHTargetSpec defines the configuration for an SSH target.
Expand Down
33 changes: 24 additions & 9 deletions api/v1alpha1/warpgatetarget_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,38 @@ func (d *WarpgateTargetCustomDefaulter) Default(ctx context.Context, target *War
target.Spec.VNC.Port = 5900
}

// Default TLS mode to defaultTLSMode for HTTP, MySQL, and PostgreSQL targets.
if target.Spec.HTTP != nil && target.Spec.HTTP.TLS != nil && target.Spec.HTTP.TLS.Mode == "" {
target.Spec.HTTP.TLS.Mode = defaultTLSMode
// Default TLS mode and verification for HTTP, MySQL, PostgreSQL, and Kubernetes targets.
if target.Spec.HTTP != nil {
defaultTLS(target.Spec.HTTP.TLS)
}
if target.Spec.MySQL != nil && target.Spec.MySQL.TLS != nil && target.Spec.MySQL.TLS.Mode == "" {
target.Spec.MySQL.TLS.Mode = defaultTLSMode
if target.Spec.MySQL != nil {
defaultTLS(target.Spec.MySQL.TLS)
}
if target.Spec.PostgreSQL != nil && target.Spec.PostgreSQL.TLS != nil && target.Spec.PostgreSQL.TLS.Mode == "" {
target.Spec.PostgreSQL.TLS.Mode = defaultTLSMode
if target.Spec.PostgreSQL != nil {
defaultTLS(target.Spec.PostgreSQL.TLS)
}
if target.Spec.Kubernetes != nil && target.Spec.Kubernetes.TLS != nil && target.Spec.Kubernetes.TLS.Mode == "" {
target.Spec.Kubernetes.TLS.Mode = defaultTLSMode
if target.Spec.Kubernetes != nil {
defaultTLS(target.Spec.Kubernetes.TLS)
}

return nil
}

// defaultTLS fills in an explicit tls block: mode defaults to Preferred and
// certificate verification defaults to on.
func defaultTLS(tls *TLSConfigSpec) {
if tls == nil {
return
}
if tls.Mode == "" {
tls.Mode = defaultTLSMode
}
if tls.Verify == nil {
v := true
tls.Verify = &v
}
}

// +kubebuilder:webhook:path=/validate-warpgate-warpgate-warp-tech-v1alpha1-warpgatetarget,mutating=false,failurePolicy=fail,sideEffects=None,groups=warpgate.warpgate.warp.tech,resources=warpgatetargets,verbs=create;update;delete,versions=v1alpha1,name=vwarpgatetarget.kb.io,admissionReviewVersions=v1

// ValidateCreate implements admission.Validator.
Expand Down
36 changes: 36 additions & 0 deletions api/v1alpha1/warpgatetarget_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,3 +501,39 @@ func TestValidate_DeleteAlwaysSucceeds(t *testing.T) {
t.Errorf("delete validation should always succeed: %v", err)
}
}

func TestDefault_TLSVerifyDefaultsToTrue(t *testing.T) {
target := validHTTPTarget()
target.Spec.HTTP.TLS = &TLSConfigSpec{Mode: "Required"}

if err := (&WarpgateTargetCustomDefaulter{}).Default(context.Background(), target); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if target.Spec.HTTP.TLS.Verify == nil || !*target.Spec.HTTP.TLS.Verify {
t.Errorf("expected verify to default to true, got %v", target.Spec.HTTP.TLS.Verify)
}
}

func TestDefault_TLSVerifyFalsePreserved(t *testing.T) {
target := validMySQLTarget()
target.Spec.MySQL.TLS = &TLSConfigSpec{Mode: "Preferred", Verify: boolPtr(false)}

if err := (&WarpgateTargetCustomDefaulter{}).Default(context.Background(), target); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if target.Spec.MySQL.TLS.Verify == nil || *target.Spec.MySQL.TLS.Verify {
t.Errorf("expected explicit verify=false to be kept, got %v", target.Spec.MySQL.TLS.Verify)
}
}

func TestDefault_TLSAbsentStaysAbsent(t *testing.T) {
target := validPostgreSQLTarget()
target.Spec.PostgreSQL.TLS = nil

if err := (&WarpgateTargetCustomDefaulter{}).Default(context.Background(), target); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if target.Spec.PostgreSQL.TLS != nil {
t.Errorf("expected no tls block to be added, got %+v", target.Spec.PostgreSQL.TLS)
}
}
Loading
Loading