From 92ea6dbada77ca1688fd447214ff2067669a7a86 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Sat, 11 Jul 2026 02:20:31 -0300 Subject: [PATCH 1/6] feat: Add WINRM logic --- go.mod | 10 +- go.sum | 47 ++-- packages/cmd/relay.go | 1 + packages/gateway-v2/gateway.go | 14 + packages/gateway-v2/winrm/winrm.go | 238 ++++++++++++++++ .../gateway-v2/winrm/winrm_transport_test.go | 57 ++++ packages/gateway-v2/winrm_handler.go | 258 ++++++++++++++++++ 7 files changed, 598 insertions(+), 27 deletions(-) create mode 100644 packages/gateway-v2/winrm/winrm.go create mode 100644 packages/gateway-v2/winrm/winrm_transport_test.go create mode 100644 packages/gateway-v2/winrm_handler.go diff --git a/go.mod b/go.mod index d24e131e..78e4ba33 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/infisical/infisical-kmip v0.3.19 github.com/jackc/pgx/v5 v5.9.2 github.com/jcmturner/gokrb5/v8 v8.4.4 + github.com/masterzen/winrm v0.0.0-20260407182533-5570be7f80cf github.com/mattn/go-isatty v0.0.20 github.com/miekg/pkcs11 v1.1.1 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a @@ -50,6 +51,7 @@ require ( golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 golang.org/x/sys v0.46.0 golang.org/x/term v0.44.0 + gopkg.in/ini.v1 v1.62.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.4 @@ -64,6 +66,7 @@ require ( cloud.google.com/go/iam v1.1.11 // indirect dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.1 // indirect + github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/alessio/shellescape v1.4.1 // indirect @@ -82,6 +85,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect github.com/aws/smithy-go v1.20.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b // indirect + github.com/bodgit/windows v1.0.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -106,6 +111,7 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.5 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -121,6 +127,7 @@ require ( github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gosimple/slug v1.15.0 // indirect github.com/gosimple/unidecode v1.0.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -138,6 +145,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -174,6 +182,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect + github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -206,7 +215,6 @@ require ( google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect diff --git a/go.sum b/go.sum index 70eea6db..25dd036b 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8 github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= +github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8= github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto= github.com/Infisical/turn/v4 v4.0.1 h1:omdelNsnFfzS5cu86W5OBR68by68a8sva4ogR0lQQnw= @@ -111,6 +113,10 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b h1:baFN6AnR0SeC194X2D292IUZcHDs4JjStpqtE70fjXE= +github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b/go.mod h1:Ram6ngyPDmP+0t6+4T2rymv0w0BS9N8Ch5vvUJccw5o= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -235,6 +241,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= @@ -332,10 +340,12 @@ github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= @@ -347,6 +357,8 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -437,13 +449,14 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= +github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= +github.com/masterzen/winrm v0.0.0-20260407182533-5570be7f80cf h1:UxGs98qiSWMqoqQsJxSW4FzCRdPPUFCraQ74ufgmISI= +github.com/masterzen/winrm v0.0.0-20260407182533-5570be7f80cf/go.mod h1:JajVhkiG2bYSNYYPYuWG7WZHr42CTjMTcCjfInRNCqc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -559,9 +572,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= -github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -625,6 +635,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0= +github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde/go.mod h1:MvrEmduDUz4ST5pGZ7CABCnOU5f3ZiOAZzT6b1A6nX8= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/wasilibs/go-re2 v1.10.0 h1:vQZEBYZOCA9jdBMmrO4+CvqyCj0x4OomXTJ4a5/urQ0= @@ -654,7 +666,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= @@ -718,13 +729,10 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -802,7 +810,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -838,8 +845,6 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -887,11 +892,9 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -899,8 +902,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -909,8 +910,6 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -926,8 +925,6 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -988,12 +985,10 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/packages/cmd/relay.go b/packages/cmd/relay.go index ace0a7c8..0264a27b 100644 --- a/packages/cmd/relay.go +++ b/packages/cmd/relay.go @@ -383,6 +383,7 @@ func init() { relayStartCmd.Flags().String("domain", "", "domain of your self-hosted Infisical instance (used with --enroll-method)") relayStartCmd.Flags().String("client-id", "", "client id for universal auth") relayStartCmd.Flags().String("client-secret", "", "client secret for universal auth") + relayStartCmd.Flags().String("organization-slug", "", "When set, this will scope the login session to the specified sub-organization the machine identity has access to. If left empty, the session defaults to the organization where the machine identity was created in.") relayStartCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") relayStartCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") relayStartCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") diff --git a/packages/gateway-v2/gateway.go b/packages/gateway-v2/gateway.go index d3a1cc5b..540b0df4 100644 --- a/packages/gateway-v2/gateway.go +++ b/packages/gateway-v2/gateway.go @@ -42,6 +42,7 @@ const ( ForwardModeHealth ForwardMode = "HEALTH" ForwardModePkcs11 ForwardMode = "PKCS11" ForwardModeADCS ForwardMode = "ADCS" + ForwardModeWinRM ForwardMode = "WINRM" ) type ActorType string @@ -1003,6 +1004,14 @@ func (g *Gateway) handleIncomingChannel(newChannel ssh.NewChannel) { log.Info().Msg("ADCS handler completed") } return + } else if forwardConfig.Mode == ForwardModeWinRM { + log.Info().Msg("Starting WinRM handler") + if err := serveWinrmOverTLS(g.ctx, tlsConn, reader, forwardConfig.TargetHost, forwardConfig.TargetPort); err != nil { + log.Error().Err(err).Msg("WinRM handler ended with error") + } else { + log.Info().Msg("WinRM handler completed") + } + return } } @@ -1065,6 +1074,10 @@ func (g *Gateway) parseForwardConfigFromALPN(tlsConn *tls.Conn, reader *bufio.Re config.Mode = ForwardModeADCS return config, nil + case "infisical-winrm": + config.Mode = ForwardModeWinRM + return config, nil + default: return nil, fmt.Errorf("unsupported ALPN protocol: %s", negotiatedProtocol) } @@ -1239,6 +1252,7 @@ func nextProtosForGateway(pkcs11Loaded bool) []string { "infisical-pam-session-cancellation", "infisical-pam-capabilities", "infisical-adcs", + "infisical-winrm", } if pkcs11Loaded { base = append(base, "infisical-pkcs11") diff --git a/packages/gateway-v2/winrm/winrm.go b/packages/gateway-v2/winrm/winrm.go new file mode 100644 index 00000000..94a99fff --- /dev/null +++ b/packages/gateway-v2/winrm/winrm.go @@ -0,0 +1,238 @@ +// Package winrm delivers files to, and runs commands on, Windows hosts over WinRM +// on behalf of the Infisical control plane, which cannot reach the host directly. +package winrm + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strings" + "time" + "unicode/utf8" + + "github.com/masterzen/winrm" +) + +// ErrConnect marks a failure to reach or authenticate to the Windows host. +var ErrConnect = errors.New("failed to connect to the Windows host over WinRM") + +const commandDeadline = 60 * time.Second + +type Credentials struct { + Host string + Port int + Username string + Password string + UseHTTPS bool + // Insecure skips TLS verification (HTTPS only); ignored for plain HTTP. + Insecure bool +} + +type FileDelivery struct { + Path string + Content []byte +} + +// transportDecorator selects the WinRM transport: NTLM message encryption over plain HTTP so the +// payload is never sent in cleartext, or plain NTLM auth over HTTPS where TLS handles confidentiality. +func transportDecorator(useHTTPS bool) (func() winrm.Transporter, error) { + if useHTTPS { + return func() winrm.Transporter { return &winrm.ClientNTLM{} }, nil + } + enc, err := winrm.NewEncryption("ntlm") + if err != nil { + return nil, fmt.Errorf("%w: failed to initialize NTLM message encryption: %v", ErrConnect, err) + } + return func() winrm.Transporter { return enc }, nil +} + +func newClient(creds Credentials) (*winrm.Client, error) { + params := *winrm.DefaultParameters + + decorator, err := transportDecorator(creds.UseHTTPS) + if err != nil { + return nil, err + } + params.TransportDecorator = decorator + + endpoint := winrm.NewEndpoint( + creds.Host, + creds.Port, + creds.UseHTTPS, + creds.Insecure, + nil, nil, nil, + commandDeadline, + ) + client, err := winrm.NewClientWithParameters(endpoint, creds.Username, creds.Password, ¶ms) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrConnect, err) + } + return client, nil +} + +// run executes a PowerShell script and returns its trimmed stdout; a non-zero exit is an error. +func run(ctx context.Context, client *winrm.Client, script string) (string, error) { + var stdout, stderr bytes.Buffer + code, err := client.RunWithContext(ctx, winrm.Powershell(script), &stdout, &stderr) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrConnect, err) + } + if code != 0 { + msg := strings.TrimSpace(stderr.String()) + if msg == "" { + msg = strings.TrimSpace(stdout.String()) + } + return "", fmt.Errorf("command failed (exit %d): %s", code, truncate(msg, 500)) + } + return strings.TrimSpace(stdout.String()), nil +} + +// Ping proves reachability and authentication without touching the filesystem. +func Ping(ctx context.Context, creds Credentials) error { + client, err := newClient(creds) + if err != nil { + return err + } + _, err = run(ctx, client, `$ProgressPreference='SilentlyContinue'; Write-Output ('OK='+[System.Environment]::MachineName)`) + return err +} + +// b64ChunkSize keeps each PowerShell command under the Windows command-line limit (~8191 chars), +// so a file's base64 is appended to a staging file in chunks rather than inlined in one command. +const b64ChunkSize = 2000 + +// DeliverFiles writes each file atomically (temp file + Move-Item), creating the parent dir if missing. +func DeliverFiles(ctx context.Context, creds Credentials, files []FileDelivery) error { + client, err := newClient(creds) + if err != nil { + return err + } + // Clear staging files left by a previously interrupted delivery, once per directory. + swept := map[string]bool{} + for _, f := range files { + dir := winParentDir(f.Path) + if dir != "" && !swept[dir] { + swept[dir] = true + sweepStaleStaging(ctx, client, dir) + } + } + for _, f := range files { + if err := deliverOne(ctx, client, f); err != nil { + return fmt.Errorf("failed to write %q: %w", f.Path, err) + } + } + return nil +} + +// stagingMarker suffixes staging files so a sweep can find leftovers; the random token avoids collisions. +const stagingMarker = ".infisical." + +func winParentDir(p string) string { + i := strings.LastIndexAny(p, "\\/") + if i <= 0 { + return "" + } + return p[:i] +} + +// sweepStaleStaging best-effort removes stagingMarker files older than 60s; failures never block a delivery. +func sweepStaleStaging(ctx context.Context, client *winrm.Client, dir string) { + script := fmt.Sprintf( + `$ErrorActionPreference='SilentlyContinue'; $d='%s'; `+ + `if (Test-Path -LiteralPath $d) { $cut=(Get-Date).AddSeconds(-60); `+ + `Get-ChildItem -LiteralPath $d -File -Filter '*%s*' | Where-Object { $_.LastWriteTime -lt $cut } | Remove-Item -Force }`, + psSingleQuote(dir), stagingMarker, + ) + _, _ = run(ctx, client, script) +} + +func deliverOne(ctx context.Context, client *winrm.Client, f FileDelivery) error { + pathLit := psSingleQuote(f.Path) + b64 := base64.StdEncoding.EncodeToString(f.Content) + token := make([]byte, 6) + if _, err := rand.Read(token); err != nil { + return fmt.Errorf("failed to generate staging token: %w", err) + } + b64Ext := fmt.Sprintf("%s%s.b64", stagingMarker, hex.EncodeToString(token)) + tmpExt := fmt.Sprintf("%s%s.tmp", stagingMarker, hex.EncodeToString(token)) + + init := fmt.Sprintf( + `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; `+ + `$p='%s'; New-Item -ItemType Directory -Force -Path (Split-Path -Parent $p) | Out-Null; `+ + `$b64=$p+'%s'; if (Test-Path -LiteralPath $b64) { Remove-Item -Force -LiteralPath $b64 }; `+ + `New-Item -ItemType File -Force -Path $b64 | Out-Null`, + pathLit, b64Ext, + ) + if _, err := run(ctx, client, init); err != nil { + return err + } + + for i := 0; i < len(b64); i += b64ChunkSize { + end := i + b64ChunkSize + if end > len(b64) { + end = len(b64) + } + // base64 is [A-Za-z0-9+/=] only, so it needs no escaping inside single quotes. + appendCmd := fmt.Sprintf( + `$ErrorActionPreference='Stop'; $b64='%s'+'%s'; `+ + `[IO.File]::AppendAllText($b64,'%s')`, + pathLit, b64Ext, b64[i:end], + ) + if _, err := run(ctx, client, appendCmd); err != nil { + return err + } + } + + // Verify the file exists after the move so a silently-normalized destination isn't reported as success. + finalize := fmt.Sprintf( + `$ErrorActionPreference='Stop'; $p='%s'; $b64=$p+'%s'; $tmp=$p+'%s'; `+ + `[IO.File]::WriteAllBytes($tmp,[Convert]::FromBase64String([IO.File]::ReadAllText($b64))); `+ + `Move-Item -Force -LiteralPath $tmp -Destination $p; Remove-Item -Force -LiteralPath $b64; `+ + `if (-not (Test-Path -LiteralPath $p)) { throw ('file not found after write: '+$p) }; `+ + `Write-Output ('WROTE='+$p)`, + pathLit, b64Ext, tmpExt, + ) + if _, err := run(ctx, client, finalize); err != nil { + return err + } + return nil +} + +// RemoveFiles deletes each path if it exists. A missing file is not an error. +func RemoveFiles(ctx context.Context, creds Credentials, paths []string) error { + client, err := newClient(creds) + if err != nil { + return err + } + for _, p := range paths { + script := fmt.Sprintf( + `$ErrorActionPreference='Stop'; $p='%s'; `+ + `if (Test-Path -LiteralPath $p) { Remove-Item -Force -LiteralPath $p }; Write-Output ('REMOVED='+$p)`, + psSingleQuote(p), + ) + if _, err := run(ctx, client, script); err != nil { + return fmt.Errorf("failed to remove %q: %w", p, err) + } + } + return nil +} + +// psSingleQuote escapes a value for a PowerShell single-quoted string by doubling the single quote. +func psSingleQuote(s string) string { + return strings.ReplaceAll(s, "'", "''") +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + // Back up to a rune boundary so we don't split a multibyte character and emit invalid UTF-8. + for n > 0 && !utf8.RuneStart(s[n]) { + n-- + } + return s[:n] + "…" +} diff --git a/packages/gateway-v2/winrm/winrm_transport_test.go b/packages/gateway-v2/winrm/winrm_transport_test.go new file mode 100644 index 00000000..8ae0f93d --- /dev/null +++ b/packages/gateway-v2/winrm/winrm_transport_test.go @@ -0,0 +1,57 @@ +package winrm + +import ( + "sync" + "testing" + + "github.com/masterzen/winrm" +) + +// TestTransportDecorator locks in the transport split: NTLM message encryption over HTTP, plain NTLM auth over HTTPS. +func TestTransportDecorator(t *testing.T) { + httpDec, err := transportDecorator(false) + if err != nil { + t.Fatalf("transportDecorator(false): %v", err) + } + if _, ok := httpDec().(*winrm.Encryption); !ok { + t.Errorf("HTTP: expected *winrm.Encryption, got %T", httpDec()) + } + + httpsDec, err := transportDecorator(true) + if err != nil { + t.Fatalf("transportDecorator(true): %v", err) + } + if _, ok := httpsDec().(*winrm.ClientNTLM); !ok { + t.Errorf("HTTPS: expected *winrm.ClientNTLM, got %T", httpsDec()) + } +} + +// TestNewClientDoesNotMutateGlobalParameters guards the pointer-alias fix: newClient must not write +// TransportDecorator back onto the shared winrm.DefaultParameters global. +func TestNewClientDoesNotMutateGlobalParameters(t *testing.T) { + winrm.DefaultParameters.TransportDecorator = nil + if _, err := newClient(Credentials{Host: "127.0.0.1", Port: 5985, Username: "u", Password: "p"}); err != nil { + t.Fatalf("newClient: %v", err) + } + if winrm.DefaultParameters.TransportDecorator != nil { + t.Fatal("newClient mutated the shared winrm.DefaultParameters global (pointer-alias regression)") + } +} + +// TestNewClientConcurrent builds clients across both transports concurrently; run with -race it +// guards against the shared-DefaultParameters data race. +func TestNewClientConcurrent(t *testing.T) { + var wg sync.WaitGroup + for i := 0; i < 50; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + creds := Credentials{Host: "127.0.0.1", Port: 5985, Username: "u", Password: "p", UseHTTPS: i%2 == 1} + c, err := newClient(creds) + if err != nil || c == nil { + t.Errorf("newClient(useHTTPS=%v): client=%v err=%v", creds.UseHTTPS, c, err) + } + }(i) + } + wg.Wait() +} diff --git a/packages/gateway-v2/winrm_handler.go b/packages/gateway-v2/winrm_handler.go new file mode 100644 index 00000000..478b3a52 --- /dev/null +++ b/packages/gateway-v2/winrm_handler.go @@ -0,0 +1,258 @@ +package gatewayv2 + +import ( + "bufio" + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "github.com/Infisical/infisical-merge/packages/gateway-v2/winrm" + "github.com/rs/zerolog/log" +) + +// winrmRequestEnvelope is the common body for every WinRM operation. Host/port come from the signed +// routing extension, not this body, so a caller cannot point the gateway at an arbitrary host (SSRF). +type winrmRequestEnvelope struct { + Username string `json:"username"` + Password string `json:"password"` + Params json.RawMessage `json:"params"` +} + +// winrmTargetContextKey carries the target host/port into the operation handlers. +type winrmTargetContextKey struct{} + +type winrmTarget struct { + Host string + Port int +} + +func winrmTargetFromContext(ctx context.Context) winrmTarget { + t, _ := ctx.Value(winrmTargetContextKey{}).(winrmTarget) + return t +} + +// winrmTransportParams are the per-request transport settings; host/port are excluded (see envelope). +type winrmTransportParams struct { + UseHTTPS bool `json:"useHttps"` + Insecure bool `json:"insecure"` +} + +type winrmDeliverFile struct { + Path string `json:"path"` + ContentBase64 string `json:"contentBase64"` +} + +type winrmDeliverParams struct { + winrmTransportParams + Files []winrmDeliverFile `json:"files"` +} + +type winrmRemoveParams struct { + winrmTransportParams + Paths []string `json:"paths"` +} + +type winrmResponse struct { + Result json.RawMessage `json:"result"` +} + +type winrmErrorResponse struct { + Error winrmErrorBody `json:"error"` +} + +type winrmErrorBody struct { + Message string `json:"message"` +} + +const ( + // winrmConnDeadline is longer than winrmOpDeadline so the gateway can flush a structured + // error response before the connection deadline trips. + winrmOpDeadline = 120 * time.Second + winrmConnDeadline = winrmOpDeadline + 15*time.Second + maxWinrmRequestBodyBytes = 4 * 1024 * 1024 +) + +// serveWinrmOverTLS reads a single HTTP request off the TLS relay connection and dispatches it to the mux. +func serveWinrmOverTLS(ctx context.Context, conn *tls.Conn, reader *bufio.Reader, targetHost string, targetPort int) error { + _ = conn.SetDeadline(time.Now().Add(winrmConnDeadline)) + + reqCh := make(chan *http.Request, 1) + errCh := make(chan error, 1) + go func() { + req, err := http.ReadRequest(reader) + if err != nil { + errCh <- err + return + } + reqCh <- req + }() + + var req *http.Request + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errCh: + return fmt.Errorf("failed to read HTTP request: %w", err) + case req = <-reqCh: + } + + log.Debug().Str("path", req.URL.Path).Msg("winrm: request received") + + opCtx, cancel := context.WithTimeout(ctx, winrmOpDeadline) + defer cancel() + opCtx = context.WithValue(opCtx, winrmTargetContextKey{}, winrmTarget{Host: targetHost, Port: targetPort}) + req = req.WithContext(opCtx) + + rw := newBufferedResponseWriter() + serveWinrmMux().ServeHTTP(rw, req) + if err := rw.writeTo(conn); err != nil { + return fmt.Errorf("failed to write response: %w", err) + } + log.Debug().Int("status", rw.status).Msg("winrm: response written") + return nil +} + +var serveWinrmMux = sync.OnceValue(func() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/v1/test", wrapWinrm(handleWinrmTest)) + mux.HandleFunc("/v1/deliver", wrapWinrm(handleWinrmDeliver)) + mux.HandleFunc("/v1/remove", wrapWinrm(handleWinrmRemove)) + return mux +}) + +type winrmHandlerFn func(ctx context.Context, env *winrmRequestEnvelope) (any, error) + +// wrapWinrm decodes the envelope, runs the operation, and encodes the JSON result or error. It +// recovers from panics so a malformed host response can't take down the gateway process. +func wrapWinrm(fn winrmHandlerFn) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + defer func() { + if r.Body != nil { + _ = r.Body.Close() + } + if p := recover(); p != nil { + log.Error().Interface("panic", p).Str("path", r.URL.Path).Msg("winrm: recovered from panic") + writeWinrmError(w, http.StatusInternalServerError, "Internal error handling WinRM request") + } + }() + + if r.Method != http.MethodPost { + writeWinrmError(w, http.StatusMethodNotAllowed, "Only POST is supported") + return + } + if r.ContentLength > maxWinrmRequestBodyBytes { + writeWinrmError(w, http.StatusRequestEntityTooLarge, "Request body too large") + return + } + r.Body = http.MaxBytesReader(w, r.Body, maxWinrmRequestBodyBytes) + + var env winrmRequestEnvelope + if err := json.NewDecoder(r.Body).Decode(&env); err != nil { + writeWinrmError(w, http.StatusBadRequest, "Malformed request body") + return + } + ctx := r.Context() + target := winrmTargetFromContext(ctx) + if target.Host == "" || target.Port == 0 || env.Username == "" { + writeWinrmError(w, http.StatusBadRequest, "Target host, port and username are required") + return + } + + result, err := fn(ctx, &env) + if err != nil { + log.Warn().Err(err).Str("path", r.URL.Path).Msg("winrm: operation failed") + // Hide the internal host/port from the control plane on connect/auth failures. + if errors.Is(err, winrm.ErrConnect) { + err = winrm.ErrConnect + } + writeWinrmError(w, http.StatusBadGateway, err.Error()) + return + } + + raw, err := json.Marshal(result) + if err != nil { + writeWinrmError(w, http.StatusInternalServerError, "Failed to marshal result") + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(winrmResponse{Result: raw}); err != nil { + log.Warn().Err(err).Msg("winrm: failed to encode response") + } + } +} + +func credsFromEnv(ctx context.Context, env *winrmRequestEnvelope, tp winrmTransportParams) winrm.Credentials { + target := winrmTargetFromContext(ctx) + return winrm.Credentials{ + Host: target.Host, + Port: target.Port, + Username: env.Username, + Password: env.Password, + UseHTTPS: tp.UseHTTPS, + Insecure: tp.Insecure, + } +} + +func handleWinrmTest(ctx context.Context, env *winrmRequestEnvelope) (any, error) { + var tp winrmTransportParams + if len(env.Params) > 0 { + _ = json.Unmarshal(env.Params, &tp) + } + if err := winrm.Ping(ctx, credsFromEnv(ctx, env, tp)); err != nil { + return nil, err + } + return map[string]any{"ok": true}, nil +} + +func handleWinrmDeliver(ctx context.Context, env *winrmRequestEnvelope) (any, error) { + var p winrmDeliverParams + if err := json.Unmarshal(env.Params, &p); err != nil { + return nil, fmt.Errorf("malformed deliver params") + } + if len(p.Files) == 0 { + return nil, fmt.Errorf("no files to deliver") + } + files := make([]winrm.FileDelivery, 0, len(p.Files)) + for _, f := range p.Files { + if f.Path == "" { + return nil, fmt.Errorf("file path is required") + } + content, err := base64.StdEncoding.DecodeString(f.ContentBase64) + if err != nil { + return nil, fmt.Errorf("file content is not valid base64") + } + files = append(files, winrm.FileDelivery{Path: f.Path, Content: content}) + } + if err := winrm.DeliverFiles(ctx, credsFromEnv(ctx, env, p.winrmTransportParams), files); err != nil { + return nil, err + } + return map[string]any{"delivered": len(files)}, nil +} + +func handleWinrmRemove(ctx context.Context, env *winrmRequestEnvelope) (any, error) { + var p winrmRemoveParams + if err := json.Unmarshal(env.Params, &p); err != nil { + return nil, fmt.Errorf("malformed remove params") + } + if len(p.Paths) == 0 { + return map[string]any{"removed": 0}, nil + } + if err := winrm.RemoveFiles(ctx, credsFromEnv(ctx, env, p.winrmTransportParams), p.Paths); err != nil { + return nil, err + } + return map[string]any{"removed": len(p.Paths)}, nil +} + +func writeWinrmError(w http.ResponseWriter, status int, message string) { + body, _ := json.Marshal(winrmErrorResponse{Error: winrmErrorBody{Message: message}}) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write(body) +} From 59b7e4f3ab2ea51d2b6a1a848586ebb62642e45c Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Sat, 11 Jul 2026 03:26:49 -0300 Subject: [PATCH 2/6] Address greptile comments --- e2e/go.mod | 7 +++++++ e2e/go.sum | 14 ++++++++++++++ packages/gateway-v2/winrm/winrm.go | 14 +++++++++++++- packages/gateway-v2/winrm_handler.go | 4 +++- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/e2e/go.mod b/e2e/go.mod index 1e8fe9bd..5e8e25b9 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -36,6 +36,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/BobuSumisu/aho-corasick v1.0.3 // indirect + github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect @@ -63,6 +64,8 @@ require ( github.com/aws/smithy-go v1.20.3 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b // indirect + github.com/bodgit/windows v1.0.1 // indirect github.com/buger/goterm v1.0.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect @@ -126,6 +129,7 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/flock v0.12.1 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/glog v1.2.5 // indirect @@ -179,6 +183,8 @@ require ( github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/manifoldco/promptui v0.9.0 // indirect + github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect + github.com/masterzen/winrm v0.0.0-20260407182533-5570be7f80cf // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect @@ -273,6 +279,7 @@ require ( github.com/subosito/gotenv v1.2.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 0aacb47d..4797fd14 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -59,6 +59,8 @@ github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8 github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= +github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw= github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8= @@ -145,6 +147,10 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngE github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b h1:baFN6AnR0SeC194X2D292IUZcHDs4JjStpqtE70fjXE= +github.com/bodgit/ntlmssp v0.0.0-20240506230425-31973bb52d9b/go.mod h1:Ram6ngyPDmP+0t6+4T2rymv0w0BS9N8Ch5vvUJccw5o= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -386,6 +392,8 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -645,6 +653,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= +github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= +github.com/masterzen/winrm v0.0.0-20260407182533-5570be7f80cf h1:UxGs98qiSWMqoqQsJxSW4FzCRdPPUFCraQ74ufgmISI= +github.com/masterzen/winrm v0.0.0-20260407182533-5570be7f80cf/go.mod h1:JajVhkiG2bYSNYYPYuWG7WZHr42CTjMTcCjfInRNCqc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -983,6 +995,8 @@ github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+C github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0= +github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde/go.mod h1:MvrEmduDUz4ST5pGZ7CABCnOU5f3ZiOAZzT6b1A6nX8= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= diff --git a/packages/gateway-v2/winrm/winrm.go b/packages/gateway-v2/winrm/winrm.go index 94a99fff..cb255060 100644 --- a/packages/gateway-v2/winrm/winrm.go +++ b/packages/gateway-v2/winrm/winrm.go @@ -91,6 +91,18 @@ func run(ctx context.Context, client *winrm.Client, script string) (string, erro return strings.TrimSpace(stdout.String()), nil } +func runSensitive(ctx context.Context, client *winrm.Client, script string) error { + var stdout, stderr bytes.Buffer + code, err := client.RunWithContext(ctx, winrm.Powershell(script), &stdout, &stderr) + if err != nil { + return fmt.Errorf("%w: %v", ErrConnect, err) + } + if code != 0 { + return fmt.Errorf("failed to stage file content (exit %d)", code) + } + return nil +} + // Ping proves reachability and authentication without touching the filesystem. func Ping(ctx context.Context, creds Credentials) error { client, err := newClient(creds) @@ -182,7 +194,7 @@ func deliverOne(ctx context.Context, client *winrm.Client, f FileDelivery) error `[IO.File]::AppendAllText($b64,'%s')`, pathLit, b64Ext, b64[i:end], ) - if _, err := run(ctx, client, appendCmd); err != nil { + if err := runSensitive(ctx, client, appendCmd); err != nil { return err } } diff --git a/packages/gateway-v2/winrm_handler.go b/packages/gateway-v2/winrm_handler.go index 478b3a52..ac3f8d8c 100644 --- a/packages/gateway-v2/winrm_handler.go +++ b/packages/gateway-v2/winrm_handler.go @@ -203,7 +203,9 @@ func credsFromEnv(ctx context.Context, env *winrmRequestEnvelope, tp winrmTransp func handleWinrmTest(ctx context.Context, env *winrmRequestEnvelope) (any, error) { var tp winrmTransportParams if len(env.Params) > 0 { - _ = json.Unmarshal(env.Params, &tp) + if err := json.Unmarshal(env.Params, &tp); err != nil { + return nil, fmt.Errorf("malformed test params") + } } if err := winrm.Ping(ctx, credsFromEnv(ctx, env, tp)); err != nil { return nil, err From ed6e5b5a17fef06a78cedfeef7fa0b976b5bafec Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Sat, 11 Jul 2026 17:39:58 -0300 Subject: [PATCH 3/6] Address veria comments --- packages/gateway-v2/winrm/winrm.go | 97 +++++++++++++++---- .../gateway-v2/winrm/winrm_transport_test.go | 38 +++----- packages/gateway-v2/winrm_handler.go | 8 +- 3 files changed, 97 insertions(+), 46 deletions(-) diff --git a/packages/gateway-v2/winrm/winrm.go b/packages/gateway-v2/winrm/winrm.go index cb255060..1cd5e334 100644 --- a/packages/gateway-v2/winrm/winrm.go +++ b/packages/gateway-v2/winrm/winrm.go @@ -6,10 +6,13 @@ import ( "bytes" "context" "crypto/rand" + "crypto/x509" "encoding/base64" "encoding/hex" + "encoding/pem" "errors" "fmt" + "net" "strings" "time" "unicode/utf8" @@ -27,8 +30,10 @@ type Credentials struct { Port int Username string Password string - UseHTTPS bool - // Insecure skips TLS verification (HTTPS only); ignored for plain HTTP. + // CACert, when set, is a PEM bundle used to verify the listener's certificate, so a self-signed + // HTTPS listener can be authenticated without disabling verification. + CACert []byte + // Insecure skips TLS verification (confidentiality only, no MITM protection); use only when no CA can be pinned. Insecure bool } @@ -37,36 +42,85 @@ type FileDelivery struct { Content []byte } -// transportDecorator selects the WinRM transport: NTLM message encryption over plain HTTP so the -// payload is never sent in cleartext, or plain NTLM auth over HTTPS where TLS handles confidentiality. -func transportDecorator(useHTTPS bool) (func() winrm.Transporter, error) { - if useHTTPS { - return func() winrm.Transporter { return &winrm.ClientNTLM{} }, nil +// maxWinRMReadBytes caps bytes read from a host, bounding one that streams an endless body; command +// responses are tiny, so this never limits real use. +const maxWinRMReadBytes = 32 * 1024 * 1024 + +// limitedConn fails the read once maxWinRMReadBytes are consumed; the winrm library reads bodies unbounded. +type limitedConn struct { + net.Conn + remaining int64 +} + +func (c *limitedConn) Read(p []byte) (int, error) { + if c.remaining <= 0 { + return 0, fmt.Errorf("winrm response exceeded %d bytes", maxWinRMReadBytes) } - enc, err := winrm.NewEncryption("ntlm") - if err != nil { - return nil, fmt.Errorf("%w: failed to initialize NTLM message encryption: %v", ErrConnect, err) + if int64(len(p)) > c.remaining { + p = p[:c.remaining] } - return func() winrm.Transporter { return enc }, nil + n, err := c.Conn.Read(p) + c.remaining -= int64(n) + return n, err } -func newClient(creds Credentials) (*winrm.Client, error) { - params := *winrm.DefaultParameters +// boundedDial returns a dialer whose connection carries the operation deadline and read cap, since the +// library issues its HTTP request without a context. +func boundedDial(ctx context.Context) func(network, addr string) (net.Conn, error) { + return func(network, addr string) (net.Conn, error) { + conn, err := (&net.Dialer{Timeout: 30 * time.Second}).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + if deadline, ok := ctx.Deadline(); ok { + _ = conn.SetDeadline(deadline) + } + return &limitedConn{Conn: conn, remaining: maxWinRMReadBytes}, nil + } +} - decorator, err := transportDecorator(creds.UseHTTPS) +// pinnedServerName returns the name to verify a pinned cert against: its first DNS SAN, else its +// Common Name. Empty when no CA is pinned or the PEM cannot be parsed. +func pinnedServerName(caCert []byte) string { + if len(caCert) == 0 { + return "" + } + block, _ := pem.Decode(caCert) + if block == nil { + return "" + } + cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return nil, err + return "" } - params.TransportDecorator = decorator + if len(cert.DNSNames) > 0 { + return cert.DNSNames[0] + } + return cert.Subject.CommonName +} + +// newClient builds a WinRM client over HTTPS only: the library's HTTP transport can fall back to +// cleartext, whereas TLS guarantees confidentiality regardless of the NTLM layer. +func newClient(ctx context.Context, creds Credentials) (*winrm.Client, error) { + params := *winrm.DefaultParameters + params.TransportDecorator = func() winrm.Transporter { return winrm.NewClientNTLMWithDial(boundedDial(ctx)) } endpoint := winrm.NewEndpoint( creds.Host, creds.Port, - creds.UseHTTPS, + true, // HTTPS only creds.Insecure, - nil, nil, nil, + creds.CACert, // verify the listener against this CA when provided + nil, nil, commandDeadline, ) + + // Verify a pinned cert against its own name, not the connection host: WinRM hosts are often reached + // by IP while the listener cert is issued for the machine name. Chain validation still stops a MITM. + if name := pinnedServerName(creds.CACert); name != "" { + endpoint.TLSServerName = name + } + client, err := winrm.NewClientWithParameters(endpoint, creds.Username, creds.Password, ¶ms) if err != nil { return nil, fmt.Errorf("%w: %v", ErrConnect, err) @@ -91,6 +145,7 @@ func run(ctx context.Context, client *winrm.Client, script string) (string, erro return strings.TrimSpace(stdout.String()), nil } +// runSensitive is run for scripts whose text carries file content, returning a generic error that never echoes output. func runSensitive(ctx context.Context, client *winrm.Client, script string) error { var stdout, stderr bytes.Buffer code, err := client.RunWithContext(ctx, winrm.Powershell(script), &stdout, &stderr) @@ -105,7 +160,7 @@ func runSensitive(ctx context.Context, client *winrm.Client, script string) erro // Ping proves reachability and authentication without touching the filesystem. func Ping(ctx context.Context, creds Credentials) error { - client, err := newClient(creds) + client, err := newClient(ctx, creds) if err != nil { return err } @@ -119,7 +174,7 @@ const b64ChunkSize = 2000 // DeliverFiles writes each file atomically (temp file + Move-Item), creating the parent dir if missing. func DeliverFiles(ctx context.Context, creds Credentials, files []FileDelivery) error { - client, err := newClient(creds) + client, err := newClient(ctx, creds) if err != nil { return err } @@ -216,7 +271,7 @@ func deliverOne(ctx context.Context, client *winrm.Client, f FileDelivery) error // RemoveFiles deletes each path if it exists. A missing file is not an error. func RemoveFiles(ctx context.Context, creds Credentials, paths []string) error { - client, err := newClient(creds) + client, err := newClient(ctx, creds) if err != nil { return err } diff --git a/packages/gateway-v2/winrm/winrm_transport_test.go b/packages/gateway-v2/winrm/winrm_transport_test.go index 8ae0f93d..b15cbcdd 100644 --- a/packages/gateway-v2/winrm/winrm_transport_test.go +++ b/packages/gateway-v2/winrm/winrm_transport_test.go @@ -1,28 +1,23 @@ package winrm import ( + "context" "sync" "testing" "github.com/masterzen/winrm" ) -// TestTransportDecorator locks in the transport split: NTLM message encryption over HTTP, plain NTLM auth over HTTPS. -func TestTransportDecorator(t *testing.T) { - httpDec, err := transportDecorator(false) - if err != nil { - t.Fatalf("transportDecorator(false): %v", err) - } - if _, ok := httpDec().(*winrm.Encryption); !ok { - t.Errorf("HTTP: expected *winrm.Encryption, got %T", httpDec()) - } - - httpsDec, err := transportDecorator(true) +// TestNewClientUsesHTTPSTransport locks in the HTTPS-only decision: newClient must always build the +// NTLM-over-HTTPS transport, never the plain-HTTP encryption transport (which can fail open). +func TestNewClientUsesHTTPSTransport(t *testing.T) { + winrm.DefaultParameters.TransportDecorator = nil + client, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5986, Username: "u", Password: "p"}) if err != nil { - t.Fatalf("transportDecorator(true): %v", err) + t.Fatalf("newClient: %v", err) } - if _, ok := httpsDec().(*winrm.ClientNTLM); !ok { - t.Errorf("HTTPS: expected *winrm.ClientNTLM, got %T", httpsDec()) + if _, ok := client.Parameters.TransportDecorator().(*winrm.ClientNTLM); !ok { + t.Errorf("expected *winrm.ClientNTLM transport, got %T", client.Parameters.TransportDecorator()) } } @@ -30,7 +25,7 @@ func TestTransportDecorator(t *testing.T) { // TransportDecorator back onto the shared winrm.DefaultParameters global. func TestNewClientDoesNotMutateGlobalParameters(t *testing.T) { winrm.DefaultParameters.TransportDecorator = nil - if _, err := newClient(Credentials{Host: "127.0.0.1", Port: 5985, Username: "u", Password: "p"}); err != nil { + if _, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5986, Username: "u", Password: "p"}); err != nil { t.Fatalf("newClient: %v", err) } if winrm.DefaultParameters.TransportDecorator != nil { @@ -38,20 +33,19 @@ func TestNewClientDoesNotMutateGlobalParameters(t *testing.T) { } } -// TestNewClientConcurrent builds clients across both transports concurrently; run with -race it -// guards against the shared-DefaultParameters data race. +// TestNewClientConcurrent builds clients concurrently; run with -race it guards against the +// shared-DefaultParameters data race. func TestNewClientConcurrent(t *testing.T) { var wg sync.WaitGroup for i := 0; i < 50; i++ { wg.Add(1) - go func(i int) { + go func() { defer wg.Done() - creds := Credentials{Host: "127.0.0.1", Port: 5985, Username: "u", Password: "p", UseHTTPS: i%2 == 1} - c, err := newClient(creds) + c, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5986, Username: "u", Password: "p"}) if err != nil || c == nil { - t.Errorf("newClient(useHTTPS=%v): client=%v err=%v", creds.UseHTTPS, c, err) + t.Errorf("newClient: client=%v err=%v", c, err) } - }(i) + }() } wg.Wait() } diff --git a/packages/gateway-v2/winrm_handler.go b/packages/gateway-v2/winrm_handler.go index ac3f8d8c..ff22f65d 100644 --- a/packages/gateway-v2/winrm_handler.go +++ b/packages/gateway-v2/winrm_handler.go @@ -38,9 +38,11 @@ func winrmTargetFromContext(ctx context.Context) winrmTarget { } // winrmTransportParams are the per-request transport settings; host/port are excluded (see envelope). +// WinRM always runs over HTTPS, so only TLS verification is configurable here: pin a CA certificate +// (PEM) to authenticate a self-signed listener, or skip verification entirely. type winrmTransportParams struct { - UseHTTPS bool `json:"useHttps"` - Insecure bool `json:"insecure"` + Insecure bool `json:"insecure"` + CACertificate string `json:"caCertificate"` } type winrmDeliverFile struct { @@ -195,8 +197,8 @@ func credsFromEnv(ctx context.Context, env *winrmRequestEnvelope, tp winrmTransp Port: target.Port, Username: env.Username, Password: env.Password, - UseHTTPS: tp.UseHTTPS, Insecure: tp.Insecure, + CACert: []byte(tp.CACertificate), } } From 87d772821b06272a06822cd0eb4ba10bb7fa081f Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 13 Jul 2026 17:05:44 -0300 Subject: [PATCH 4/6] Fix HTTP/HTTPS winrm --- packages/gateway-v2/winrm/winrm.go | 43 +++++++++++++------ .../gateway-v2/winrm/winrm_transport_test.go | 37 ++++++++++------ packages/gateway-v2/winrm_handler.go | 6 ++- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/packages/gateway-v2/winrm/winrm.go b/packages/gateway-v2/winrm/winrm.go index 1cd5e334..d2d079d2 100644 --- a/packages/gateway-v2/winrm/winrm.go +++ b/packages/gateway-v2/winrm/winrm.go @@ -30,11 +30,14 @@ type Credentials struct { Port int Username string Password string - // CACert, when set, is a PEM bundle used to verify the listener's certificate, so a self-signed - // HTTPS listener can be authenticated without disabling verification. - CACert []byte - // Insecure skips TLS verification (confidentiality only, no MITM protection); use only when no CA can be pinned. + // UseHTTPS selects the transport: false (default) uses HTTP with NTLM message encryption, which + // needs no server certificate; true uses HTTPS. + UseHTTPS bool + // Insecure skips TLS certificate verification (HTTPS only), for when the operator opts out of it. Insecure bool + // CACert, when set (HTTPS only), is a PEM bundle used to verify a self-signed listener's certificate + // so it can be authenticated without a publicly trusted CA. + CACert []byte } type FileDelivery struct { @@ -99,26 +102,40 @@ func pinnedServerName(caCert []byte) string { return cert.Subject.CommonName } -// newClient builds a WinRM client over HTTPS only: the library's HTTP transport can fall back to -// cleartext, whereas TLS guarantees confidentiality regardless of the NTLM layer. +// newClient builds a WinRM client. HTTP (the default) uses NTLM message encryption to keep the SOAP +// body confidential without a server certificate; HTTPS uses TLS, verifying the listener against the +// system trust store, an optional pinned CA (for a self-signed listener), or skipping verification +// when Insecure is set. func newClient(ctx context.Context, creds Credentials) (*winrm.Client, error) { params := *winrm.DefaultParameters - params.TransportDecorator = func() winrm.Transporter { return winrm.NewClientNTLMWithDial(boundedDial(ctx)) } + if creds.UseHTTPS { + // The bounded dial caps the response read; the library otherwise reads the body unbounded. + params.TransportDecorator = func() winrm.Transporter { return winrm.NewClientNTLMWithDial(boundedDial(ctx)) } + } else { + enc, err := winrm.NewEncryption("ntlm") + if err != nil { + return nil, fmt.Errorf("%w: failed to initialize NTLM message encryption: %v", ErrConnect, err) + } + params.TransportDecorator = func() winrm.Transporter { return enc } + } endpoint := winrm.NewEndpoint( creds.Host, creds.Port, - true, // HTTPS only + creds.UseHTTPS, creds.Insecure, - creds.CACert, // verify the listener against this CA when provided + creds.CACert, nil, nil, commandDeadline, ) - // Verify a pinned cert against its own name, not the connection host: WinRM hosts are often reached - // by IP while the listener cert is issued for the machine name. Chain validation still stops a MITM. - if name := pinnedServerName(creds.CACert); name != "" { - endpoint.TLSServerName = name + // When verifying against a pinned CA, verify the presented cert against the pinned cert's own name + // rather than the connection host: WinRM hosts are often reached by IP while the listener cert is + // issued for the machine name. Chain validation against the pinned CA still stops a MITM. + if creds.UseHTTPS && !creds.Insecure { + if name := pinnedServerName(creds.CACert); name != "" { + endpoint.TLSServerName = name + } } client, err := winrm.NewClientWithParameters(endpoint, creds.Username, creds.Password, ¶ms) diff --git a/packages/gateway-v2/winrm/winrm_transport_test.go b/packages/gateway-v2/winrm/winrm_transport_test.go index b15cbcdd..1757f31d 100644 --- a/packages/gateway-v2/winrm/winrm_transport_test.go +++ b/packages/gateway-v2/winrm/winrm_transport_test.go @@ -8,16 +8,25 @@ import ( "github.com/masterzen/winrm" ) -// TestNewClientUsesHTTPSTransport locks in the HTTPS-only decision: newClient must always build the -// NTLM-over-HTTPS transport, never the plain-HTTP encryption transport (which can fail open). -func TestNewClientUsesHTTPSTransport(t *testing.T) { +// TestNewClientTransportByScheme locks in the transport split: HTTP uses NTLM message encryption +// (*winrm.Encryption), HTTPS uses NTLM auth over TLS (*winrm.ClientNTLM). +func TestNewClientTransportByScheme(t *testing.T) { winrm.DefaultParameters.TransportDecorator = nil - client, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5986, Username: "u", Password: "p"}) + + httpClient, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5985, Username: "u", Password: "p"}) if err != nil { - t.Fatalf("newClient: %v", err) + t.Fatalf("newClient(http): %v", err) + } + if _, ok := httpClient.Parameters.TransportDecorator().(*winrm.Encryption); !ok { + t.Errorf("HTTP: expected *winrm.Encryption, got %T", httpClient.Parameters.TransportDecorator()) + } + + httpsClient, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5986, Username: "u", Password: "p", UseHTTPS: true}) + if err != nil { + t.Fatalf("newClient(https): %v", err) } - if _, ok := client.Parameters.TransportDecorator().(*winrm.ClientNTLM); !ok { - t.Errorf("expected *winrm.ClientNTLM transport, got %T", client.Parameters.TransportDecorator()) + if _, ok := httpsClient.Parameters.TransportDecorator().(*winrm.ClientNTLM); !ok { + t.Errorf("HTTPS: expected *winrm.ClientNTLM, got %T", httpsClient.Parameters.TransportDecorator()) } } @@ -25,7 +34,7 @@ func TestNewClientUsesHTTPSTransport(t *testing.T) { // TransportDecorator back onto the shared winrm.DefaultParameters global. func TestNewClientDoesNotMutateGlobalParameters(t *testing.T) { winrm.DefaultParameters.TransportDecorator = nil - if _, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5986, Username: "u", Password: "p"}); err != nil { + if _, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5985, Username: "u", Password: "p"}); err != nil { t.Fatalf("newClient: %v", err) } if winrm.DefaultParameters.TransportDecorator != nil { @@ -33,19 +42,19 @@ func TestNewClientDoesNotMutateGlobalParameters(t *testing.T) { } } -// TestNewClientConcurrent builds clients concurrently; run with -race it guards against the -// shared-DefaultParameters data race. +// TestNewClientConcurrent builds clients across both transports concurrently; run with -race it +// guards against the shared-DefaultParameters data race. func TestNewClientConcurrent(t *testing.T) { var wg sync.WaitGroup for i := 0; i < 50; i++ { wg.Add(1) - go func() { + go func(https bool) { defer wg.Done() - c, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5986, Username: "u", Password: "p"}) + c, err := newClient(context.Background(), Credentials{Host: "127.0.0.1", Port: 5985, Username: "u", Password: "p", UseHTTPS: https}) if err != nil || c == nil { - t.Errorf("newClient: client=%v err=%v", c, err) + t.Errorf("newClient(useHTTPS=%v): client=%v err=%v", https, c, err) } - }() + }(i%2 == 0) } wg.Wait() } diff --git a/packages/gateway-v2/winrm_handler.go b/packages/gateway-v2/winrm_handler.go index ff22f65d..58da5e45 100644 --- a/packages/gateway-v2/winrm_handler.go +++ b/packages/gateway-v2/winrm_handler.go @@ -38,9 +38,10 @@ func winrmTargetFromContext(ctx context.Context) winrmTarget { } // winrmTransportParams are the per-request transport settings; host/port are excluded (see envelope). -// WinRM always runs over HTTPS, so only TLS verification is configurable here: pin a CA certificate -// (PEM) to authenticate a self-signed listener, or skip verification entirely. +// useHttps selects HTTPS over the default HTTP-with-NTLM-message-encryption transport; for HTTPS, +// insecure skips certificate verification and caCertificate pins a CA to authenticate a self-signed listener. type winrmTransportParams struct { + UseHTTPS bool `json:"useHttps"` Insecure bool `json:"insecure"` CACertificate string `json:"caCertificate"` } @@ -197,6 +198,7 @@ func credsFromEnv(ctx context.Context, env *winrmRequestEnvelope, tp winrmTransp Port: target.Port, Username: env.Username, Password: env.Password, + UseHTTPS: tp.UseHTTPS, Insecure: tp.Insecure, CACert: []byte(tp.CACertificate), } From d1be0bf175fa44ba2393ddceddd972fd5a412dea Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 13 Jul 2026 18:26:44 -0300 Subject: [PATCH 5/6] Minor improvement on variable names --- packages/cmd/relay.go | 1 - packages/gateway-v2/winrm/winrm.go | 39 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/cmd/relay.go b/packages/cmd/relay.go index 0264a27b..ace0a7c8 100644 --- a/packages/cmd/relay.go +++ b/packages/cmd/relay.go @@ -383,7 +383,6 @@ func init() { relayStartCmd.Flags().String("domain", "", "domain of your self-hosted Infisical instance (used with --enroll-method)") relayStartCmd.Flags().String("client-id", "", "client id for universal auth") relayStartCmd.Flags().String("client-secret", "", "client secret for universal auth") - relayStartCmd.Flags().String("organization-slug", "", "When set, this will scope the login session to the specified sub-organization the machine identity has access to. If left empty, the session defaults to the organization where the machine identity was created in.") relayStartCmd.Flags().String("machine-identity-id", "", "machine identity id for kubernetes, azure, gcp-id-token, gcp-iam, and aws-iam auth methods") relayStartCmd.Flags().String("service-account-token-path", "", "service account token path for kubernetes auth") relayStartCmd.Flags().String("service-account-key-file-path", "", "service account key file path for GCP IAM auth") diff --git a/packages/gateway-v2/winrm/winrm.go b/packages/gateway-v2/winrm/winrm.go index d2d079d2..0b459a3e 100644 --- a/packages/gateway-v2/winrm/winrm.go +++ b/packages/gateway-v2/winrm/winrm.go @@ -162,8 +162,9 @@ func run(ctx context.Context, client *winrm.Client, script string) (string, erro return strings.TrimSpace(stdout.String()), nil } -// runSensitive is run for scripts whose text carries file content, returning a generic error that never echoes output. -func runSensitive(ctx context.Context, client *winrm.Client, script string) error { +// runSuppressingOutput runs a script whose text carries file content, returning a generic error on +// failure that never echoes the remote stdout/stderr (which could contain that content). +func runSuppressingOutput(ctx context.Context, client *winrm.Client, script string) error { var stdout, stderr bytes.Buffer code, err := client.RunWithContext(ctx, winrm.Powershell(script), &stdout, &stderr) if err != nil { @@ -185,9 +186,9 @@ func Ping(ctx context.Context, creds Credentials) error { return err } -// b64ChunkSize keeps each PowerShell command under the Windows command-line limit (~8191 chars), +// base64ChunkSize keeps each PowerShell command under the Windows command-line limit (~8191 chars), // so a file's base64 is appended to a staging file in chunks rather than inlined in one command. -const b64ChunkSize = 2000 +const base64ChunkSize = 2000 // DeliverFiles writes each file atomically (temp file + Move-Item), creating the parent dir if missing. func DeliverFiles(ctx context.Context, creds Credentials, files []FileDelivery) error { @@ -198,14 +199,14 @@ func DeliverFiles(ctx context.Context, creds Credentials, files []FileDelivery) // Clear staging files left by a previously interrupted delivery, once per directory. swept := map[string]bool{} for _, f := range files { - dir := winParentDir(f.Path) + dir := windowsParentDir(f.Path) if dir != "" && !swept[dir] { swept[dir] = true - sweepStaleStaging(ctx, client, dir) + removeStaleStagingFiles(ctx, client, dir) } } for _, f := range files { - if err := deliverOne(ctx, client, f); err != nil { + if err := deliverFile(ctx, client, f); err != nil { return fmt.Errorf("failed to write %q: %w", f.Path, err) } } @@ -215,7 +216,7 @@ func DeliverFiles(ctx context.Context, creds Credentials, files []FileDelivery) // stagingMarker suffixes staging files so a sweep can find leftovers; the random token avoids collisions. const stagingMarker = ".infisical." -func winParentDir(p string) string { +func windowsParentDir(p string) string { i := strings.LastIndexAny(p, "\\/") if i <= 0 { return "" @@ -223,19 +224,19 @@ func winParentDir(p string) string { return p[:i] } -// sweepStaleStaging best-effort removes stagingMarker files older than 60s; failures never block a delivery. -func sweepStaleStaging(ctx context.Context, client *winrm.Client, dir string) { +// removeStaleStagingFiles best-effort removes stagingMarker files older than 60s; failures never block a delivery. +func removeStaleStagingFiles(ctx context.Context, client *winrm.Client, dir string) { script := fmt.Sprintf( `$ErrorActionPreference='SilentlyContinue'; $d='%s'; `+ `if (Test-Path -LiteralPath $d) { $cut=(Get-Date).AddSeconds(-60); `+ `Get-ChildItem -LiteralPath $d -File -Filter '*%s*' | Where-Object { $_.LastWriteTime -lt $cut } | Remove-Item -Force }`, - psSingleQuote(dir), stagingMarker, + escapePowerShellSingleQuotes(dir), stagingMarker, ) _, _ = run(ctx, client, script) } -func deliverOne(ctx context.Context, client *winrm.Client, f FileDelivery) error { - pathLit := psSingleQuote(f.Path) +func deliverFile(ctx context.Context, client *winrm.Client, f FileDelivery) error { + pathLit := escapePowerShellSingleQuotes(f.Path) b64 := base64.StdEncoding.EncodeToString(f.Content) token := make([]byte, 6) if _, err := rand.Read(token); err != nil { @@ -255,8 +256,8 @@ func deliverOne(ctx context.Context, client *winrm.Client, f FileDelivery) error return err } - for i := 0; i < len(b64); i += b64ChunkSize { - end := i + b64ChunkSize + for i := 0; i < len(b64); i += base64ChunkSize { + end := i + base64ChunkSize if end > len(b64) { end = len(b64) } @@ -266,7 +267,7 @@ func deliverOne(ctx context.Context, client *winrm.Client, f FileDelivery) error `[IO.File]::AppendAllText($b64,'%s')`, pathLit, b64Ext, b64[i:end], ) - if err := runSensitive(ctx, client, appendCmd); err != nil { + if err := runSuppressingOutput(ctx, client, appendCmd); err != nil { return err } } @@ -296,7 +297,7 @@ func RemoveFiles(ctx context.Context, creds Credentials, paths []string) error { script := fmt.Sprintf( `$ErrorActionPreference='Stop'; $p='%s'; `+ `if (Test-Path -LiteralPath $p) { Remove-Item -Force -LiteralPath $p }; Write-Output ('REMOVED='+$p)`, - psSingleQuote(p), + escapePowerShellSingleQuotes(p), ) if _, err := run(ctx, client, script); err != nil { return fmt.Errorf("failed to remove %q: %w", p, err) @@ -305,8 +306,8 @@ func RemoveFiles(ctx context.Context, creds Credentials, paths []string) error { return nil } -// psSingleQuote escapes a value for a PowerShell single-quoted string by doubling the single quote. -func psSingleQuote(s string) string { +// escapePowerShellSingleQuotes escapes a value for a PowerShell single-quoted string by doubling the single quote. +func escapePowerShellSingleQuotes(s string) string { return strings.ReplaceAll(s, "'", "''") } From 2462a6e5c3c44561ea547a904971e34272c20bc2 Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Mon, 13 Jul 2026 19:39:16 -0300 Subject: [PATCH 6/6] Minor fix on winrm --- packages/gateway-v2/winrm/winrm.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/gateway-v2/winrm/winrm.go b/packages/gateway-v2/winrm/winrm.go index 0b459a3e..9c1db803 100644 --- a/packages/gateway-v2/winrm/winrm.go +++ b/packages/gateway-v2/winrm/winrm.go @@ -235,7 +235,7 @@ func removeStaleStagingFiles(ctx context.Context, client *winrm.Client, dir stri _, _ = run(ctx, client, script) } -func deliverFile(ctx context.Context, client *winrm.Client, f FileDelivery) error { +func deliverFile(ctx context.Context, client *winrm.Client, f FileDelivery) (err error) { pathLit := escapePowerShellSingleQuotes(f.Path) b64 := base64.StdEncoding.EncodeToString(f.Content) token := make([]byte, 6) @@ -245,11 +245,24 @@ func deliverFile(ctx context.Context, client *winrm.Client, f FileDelivery) erro b64Ext := fmt.Sprintf("%s%s.b64", stagingMarker, hex.EncodeToString(token)) tmpExt := fmt.Sprintf("%s%s.tmp", stagingMarker, hex.EncodeToString(token)) + // Best-effort cleanup so a failed delivery leaves no staging files behind (the sweep is the backstop). + defer func() { + if err != nil { + cleanup := fmt.Sprintf( + `$ErrorActionPreference='SilentlyContinue'; $p='%s'; Remove-Item -Force -LiteralPath ($p+'%s'),($p+'%s')`, + pathLit, b64Ext, tmpExt, + ) + _ = runSuppressingOutput(ctx, client, cleanup) + } + }() + init := fmt.Sprintf( `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; `+ `$p='%s'; New-Item -ItemType Directory -Force -Path (Split-Path -Parent $p) | Out-Null; `+ `$b64=$p+'%s'; if (Test-Path -LiteralPath $b64) { Remove-Item -Force -LiteralPath $b64 }; `+ - `New-Item -ItemType File -Force -Path $b64 | Out-Null`, + `New-Item -ItemType File -Force -Path $b64 | Out-Null; `+ + `$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value; `+ + `icacls $b64 /inheritance:r /grant:r "*S-1-5-18:(F)" "*S-1-5-32-544:(F)" ('*'+$sid+':(F)') *>$null`, pathLit, b64Ext, ) if _, err := run(ctx, client, init); err != nil {