Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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