Skip to content

Commit 3388c9e

Browse files
Implement Edgeview Client Command Authentication
- to prepare for Edgeview-UI, controller side certificate signed Edgeview commands - extend the Edgeview JWT token to include 'Authen Type' - implement device side verifying Edgeview commands using controller's signing certs - implement remote user using SSH key pairs authentication for Edgeview commands - SSH public key can for authentication can use ConfigItem 'edgeview.authen.publickey' - Edgeview command is logged, and this patch adds the user-info either the username from controller side, or the info in the SSH public key comment Signed-off-by: naiming-zededa <[email protected]>
1 parent 875d620 commit 3388c9e

16 files changed

+616
-63
lines changed

docs/CONFIG-PROPERTIES.md

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
| msrv.prometheus.metrics.rps | integer | 1 | The maximum number of requests per second (RPS) for the Prometheus metrics endpoint. |
7676
| msrv.prometheus.metrics.burst | integer | 10 | The maximum burst size for the Prometheus metrics endpoint. |
7777
| msrv.prometheus.metrics.idletimeout.seconds | integer | 240 | The idle timeout in seconds for the Prometheus metrics endpoint. If the connection is idle for this duration, the limit is reset. |
78+
| edgeview.authen.publickey | string | "" | Specifies SSH RSA public keys for Edgeview client command authentication. The user must provide the path to the SSH private key in the client script, and the device verifies the command using one of the configured public keys. Separate multiple public keys with newline characters. |
7879

7980
## Log levels
8081

docs/EDGEVIEW-CONTAINER-API.md

+66
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,69 @@ For security reasons, the Edge-View container has all the volumes mounted in 're
2323
## Logging to Event
2424

2525
Edge-View logging is similar to other containers. For any user command received by Edge-View, it will log the client endpoint(IP address/port), the command and its parameters. The log entry will also be tagged with object-type of `log-to-event`, and the controller can optionally process those log entries and generate them as device events or alerts.
26+
27+
## Edge-View Client Authentication
28+
29+
To address limitations in controlling and tracking user access to Edge-View commands, the authentication enhancements to Edge-View aim to strengthen user authentication, enforce stricter access controls, and improve user tracking.
30+
31+
### Key Enhancements
32+
33+
1. **Authentication Enhancements**
34+
Stricter authentication methods will be implemented to ensure that commands originate from authorized sources. These sources include the Controller webpage session or authenticated remote users. JWT token is extended to support types of client authentication.
35+
36+
2. **Edgeview Policy Addition**
37+
A new optional "Controller Auth Only" item will be added to the controller Edge-View policy. This policy, definable at the project level, enforces that Edgeview commands must originate from the Controller.
38+
39+
3. **Client Authentication (Controller Cert Signed Type)**
40+
Commands issued from the Edge-View UI will be signed by the Zedcloud/Controller's private signing key, if the policy requires it. The device will verify these commands using the corresponding public certificate from the controller.
41+
42+
4. **Remote User Authentication (SSH Key Pairs Type)**
43+
Remote users can authenticate using SSH RSA keys, providing an additional secure access method for Edge-View. These public keys will be installed via the Edge-node ConfigItem "edgeview.authen.publickey". If there are changes to the "edgeview.authen.publickey" ConfigItem, any active Edge-View sessions on the Edge-node will be terminated. Users will need to re-enable the Edge-View session to apply the updated authentication settings. This ensures that only authorized keys are used for remote access.
44+
45+
5. **UserInfo Logging**
46+
A new "UserInfo" field will be added to the Edge-View command payload. This field logs the username of the user issuing the command, improving accountability and tracking. For the remote users using SSH key for authentication, the user public key comment field will be logged for the user tracking.
47+
48+
6. **Struct Definition Changes**
49+
Updates to the Zedcloud protocol buffers definition include:
50+
- Adding an `EvAuthType` enum in the `EvjwtInfo` struct to support different authentication methods (e.g., Controller Cert, SSH Keys).
51+
- Introducing a new `EvjwtInfo` struct to encapsulate JWT-related information with enhanced authentication support. The struct is defined as follows:
52+
53+
```go
54+
type EvjwtInfo struct {
55+
Dep string `json:"dep"` // dispatcher end-point string e.g. ip:port
56+
Sub string `json:"sub"` // jwt subject, the device UUID string
57+
Exp uint64 `json:"exp"` // expiration time for the token
58+
Key string `json:"key"` // key or nonce for payload hmac authentication
59+
Num uint8 `json:"num"` // number of instances, default is 1
60+
Enc bool `json:"enc"` // payload with encryption, default is authentication
61+
Aut EvAuthType `json:"aut"` // authentication type
62+
}
63+
```
64+
65+
- The `EvAuthType` enum defines the supported authentication types for Edge-View:
66+
67+
```go
68+
// EvAuthType - enum for authentication type of edge-view
69+
type EvAuthType int32
70+
71+
const (
72+
EvAuthTypeUnspecified EvAuthType = iota
73+
EvAuthTypeControllerCert
74+
EvAuthTypeSshRsaKeys
75+
)
76+
```
77+
78+
These additions enhance the flexibility and security of the Edge-View authentication mechanism by supporting multiple authentication methods and encapsulating JWT-related details in a structured format.
79+
80+
7. **Command Payload Signing and Verifying**
81+
When Edgeview authentication is enabled, the Edgeview command payload will include a new `username` field to carry the user name from the controller side. Additionally, the original `Hash auth` field will be used as the `Signature`, which is signed using either the controller's private signing key or the SSH private key, depending on the authentication type.
82+
83+
On the Edge-node side, the authentication will be verified using either the controller's public signing key or the SSH public key. This ensures secure and authenticated communication between the controller and the Edge-node.
84+
85+
Below is the diagram for the authentication type of `Controller Cert`.
86+
87+
![edgeview-authen-cert](./images/edgeview-authen-enhance-cert.png)
88+
89+
And the diagram for the `SSH Keys` type of authentication:
90+
91+
![edgeview-authen-sshkey](./images/edgeview-authen-enhance-sshkey.png)
-28.2 KB
Loading

pkg/edgeview/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/sirupsen/logrus v1.9.3
1616
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
1717
github.com/vishvananda/netlink v1.2.1-beta.2
18+
golang.org/x/crypto v0.35.0
1819
golang.org/x/sys v0.30.0
1920
golang.org/x/time v0.5.0
2021
)
@@ -37,7 +38,6 @@ require (
3738
github.com/tklauser/numcpus v0.6.0 // indirect
3839
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
3940
github.com/yusufpapurcu/wmi v1.2.3 // indirect
40-
golang.org/x/crypto v0.35.0 // indirect
4141
golang.org/x/net v0.36.0 // indirect
4242
golang.org/x/text v0.22.0 // indirect
4343
google.golang.org/protobuf v1.36.1 // indirect

pkg/edgeview/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
122122
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
123123
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
124124
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
125+
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
126+
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
125127
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
126128
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
127129
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=

pkg/edgeview/src/basics.go

+29-16
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func initOpts() {
8585
"edgeview",
8686
"global",
8787
"loguploader",
88+
"msrv",
8889
"newlogd",
8990
"nim",
9091
"nodeagent",
@@ -177,29 +178,30 @@ func isDefinedOpt(value string, optslice []string) bool {
177178
}
178179

179180
// get url and path from JWT token string
180-
func getAddrFromJWT(token string, isServer bool, instID int) (string, string, error) {
181+
func getAddrFromJWT(token string, isServer bool, instID int) (string, string, types.EvAuthType, error) {
181182
var addrport, path string
183+
var evAuth types.EvAuthType
182184
tparts := strings.Split(token, ".")
183185
if len(tparts) != 3 {
184-
return addrport, path, fmt.Errorf("no ip:port or invalid JWT")
186+
return addrport, path, evAuth, fmt.Errorf("no ip:port or invalid JWT")
185187
}
186188

187189
if instID > 0 {
188190
if instID > types.EdgeviewMaxInstNum {
189-
return addrport, path, fmt.Errorf("JWT inst number incorrect")
191+
return addrport, path, evAuth, fmt.Errorf("JWT inst number incorrect")
190192
}
191193
edgeviewInstID = instID
192194
}
193195

194196
data, err := base64.RawURLEncoding.DecodeString(tparts[1])
195197
if err != nil {
196-
return addrport, path, err
198+
return addrport, path, evAuth, err
197199
}
198200

199201
var jdata types.EvjwtInfo
200202
err = json.Unmarshal(data, &jdata)
201203
if err != nil {
202-
return addrport, path, err
204+
return addrport, path, evAuth, err
203205
}
204206

205207
var uuidStr string
@@ -210,33 +212,36 @@ func getAddrFromJWT(token string, isServer bool, instID int) (string, string, er
210212
}
211213
}
212214

215+
// get the client authentication method
216+
evAuth = types.EvAuthType(jdata.Aut)
217+
213218
if jdata.Exp == 0 || jdata.Dep == "" {
214-
return addrport, path, fmt.Errorf("read JWT data failed")
219+
return addrport, path, evAuth, fmt.Errorf("read JWT data failed")
215220
}
216221
if uuidStr != "" && jdata.Sub != uuidStr {
217-
return addrport, path, fmt.Errorf("uuid does not match JWT jti")
222+
return addrport, path, evAuth, fmt.Errorf("uuid does not match JWT jti")
218223
}
219224

220225
now := time.Now()
221226
nowSec := uint64(now.Unix())
222227
if nowSec > jdata.Exp {
223-
return addrport, path, fmt.Errorf("JWT expired %d sec ago", nowSec-jdata.Exp)
228+
return addrport, path, evAuth, fmt.Errorf("JWT expired %d sec ago", nowSec-jdata.Exp)
224229
}
225230

226231
if jdata.Num > 1 && instID < 1 {
227232
if runOnServer {
228-
return addrport, path, fmt.Errorf("Edgeview is in multi-instance mode, '-inst 1-%d' needs to be specified", jdata.Num)
233+
return addrport, path, evAuth, fmt.Errorf("Edgeview is in multi-instance mode, '-inst 1-%d' needs to be specified", jdata.Num)
229234
} else {
230235
warnStr := fmt.Sprintf("Edgeview is in multi-instance mode, use '-inst 1-%d', try '-inst 1' here", jdata.Num)
231236
fmt.Printf("%s\n", getColorStr(warnStr, colorCYAN))
232237
edgeviewInstID = 1
233238
}
234239
} else if jdata.Num == 1 && instID > 0 {
235240
if runOnServer {
236-
return addrport, path, fmt.Errorf("Edgeview is not in multi-instance mode, no need to specify inst-ID")
241+
return addrport, path, evAuth, fmt.Errorf("Edgeview is not in multi-instance mode, no need to specify inst-ID")
237242
} else {
238243
if instID > 1 {
239-
return addrport, path, fmt.Errorf("Maximum instance is 1, can not use inst %d", instID)
244+
return addrport, path, evAuth, fmt.Errorf("Maximum instance is 1, can not use inst %d", instID)
240245
}
241246
fmt.Printf("%s\n", getColorStr("Edgeview is not in multi-instance mode, instance ignored here", colorCYAN))
242247
edgeviewInstID = 0
@@ -249,7 +254,7 @@ func getAddrFromJWT(token string, isServer bool, instID int) (string, string, er
249254
if strings.Contains(jdataDep, "/") {
250255
urls := strings.SplitN(jdataDep, "/", 2)
251256
if len(urls) != 2 {
252-
return addrport, path, fmt.Errorf("JWT url invalid")
257+
return addrport, path, evAuth, fmt.Errorf("JWT url invalid")
253258
}
254259
addrport = urls[0]
255260
path = "/" + urls[1]
@@ -261,7 +266,7 @@ func getAddrFromJWT(token string, isServer bool, instID int) (string, string, er
261266
evStatus.StartedOn = now
262267
encryptVarInit(jdata)
263268

264-
return addrport, path, nil
269+
return addrport, path, evAuth, nil
265270
}
266271

267272
func checkClientIPMsg(msg string) bool {
@@ -350,26 +355,34 @@ func getBasics() {
350355

351356
if basics.server != "" {
352357
var printed bool
358+
var authenStr string
359+
if clientAuthType != types.EvAuthTypeUnspecified {
360+
authen := "certs"
361+
if clientAuthType == types.EvAuthTypeSSHRsaKeys {
362+
authen = "ssh-keys"
363+
}
364+
authenStr = fmt.Sprintf(", (authen %s)", authen)
365+
}
353366
conts := strings.Split(basics.server, "zedcloud.")
354367
if len(conts) == 2 {
355368
cont2s := strings.Split(conts[1], ".zededa.net")
356369
if len(cont2s) == 2 { // color highlight the cluster name string
357370
cluster := cont2s[0]
358371
colorCluster := getColorStr(cluster, colorYELLOW)
359372
controller := strings.Replace(basics.server, cluster, colorCluster, 1)
360-
fmt.Printf(" Controller: %s", controller)
373+
fmt.Printf(" Controller: %s%s\n", controller, authenStr)
361374
printed = true
362375
}
363376
}
364377
if !printed {
365-
fmt.Printf(" Controller: %s", basics.server)
378+
fmt.Printf(" Controller: %s%s\n", basics.server, authenStr)
366379
}
367380
}
368381

369382
if basics.release == "" {
370383
retbytes, err := os.ReadFile("/run/eve-release")
371384
if err == nil {
372-
basics.release = string(retbytes)
385+
basics.release = strings.TrimSuffix(string(retbytes), "\n")
373386
}
374387
}
375388

pkg/edgeview/src/copyfile.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func runCopy(opt string, tarDirSize *int64) error {
125125
}
126126

127127
// send file information to client side and wait for signal to start copy
128-
err = addEnvelopeAndWriteWss(jbytes, false)
128+
err = addEnvelopeAndWriteWss(jbytes, false, false)
129129
if err != nil {
130130
fmt.Printf("sign and write error: %v\n", err)
131131
return fmt.Errorf("cp command write file header error")
@@ -205,7 +205,7 @@ func runCopy(opt string, tarDirSize *int64) error {
205205
return fmt.Errorf("cp command error")
206206
}
207207

208-
err = addEnvelopeAndWriteWss(buffer[:n], false)
208+
err = addEnvelopeAndWriteWss(buffer[:n], false, false)
209209
if err != nil {
210210
fmt.Printf("file write to wss error %v\n", err)
211211
return fmt.Errorf("cp command error")
@@ -275,7 +275,7 @@ func recvCopyFile(msg []byte, fstatus *fileCopyStatus, mtype int) {
275275
fstatus.gotFileInfo = true
276276

277277
// send to server, go ahead and start transfer
278-
err = addEnvelopeAndWriteWss([]byte(startCopyMessage), false)
278+
err = addEnvelopeAndWriteWss([]byte(startCopyMessage), false, false)
279279
if err != nil {
280280
sendCopyDone("write start copy failed", err)
281281
}
@@ -438,7 +438,7 @@ func sendCopyDone(context string, err error) {
438438
if err != nil {
439439
fmt.Printf("%s error: %v\n", context, err)
440440
}
441-
err = addEnvelopeAndWriteWss([]byte(context), true)
441+
err = addEnvelopeAndWriteWss([]byte(context), true, false)
442442
if err != nil {
443443
fmt.Printf("sign and write error: %v\n", err)
444444
}

0 commit comments

Comments
 (0)