diff --git a/cmd/main.go b/cmd/main.go index 0d3b070f..397086d4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -15,6 +15,7 @@ package cmd import ( "context" + "encoding/base64" "fmt" "net" "os" @@ -22,6 +23,7 @@ import ( "time" "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/metrics" "github.com/libp2p/go-libp2p/core/network" @@ -58,6 +60,10 @@ func MainFlags() []cli.Flag { Name: "b", Usage: "Encodes the new config in base64, so it can be used as a token", }, + &cli.BoolFlag{ + Name: "p", + Usage: "Generates a ED25519 private key, converts it to protobuf serialized form, and encodes as base64 string", + }, &cli.BoolFlag{ Name: "debug", Usage: "Starts API with pprof attached", @@ -156,6 +162,22 @@ func Main() func(c *cli.Context) error { os.Exit(0) } + + if c.Bool("p") { + // Generates a new protobuf encoded priv key and exit + privkey, err := node.GenPrivKey(0) + if err != nil { + return err + } + + protoKey, err := crypto.MarshalPrivateKey(privkey) + if err != nil { + return err + } + + fmt.Printf("Private key: %s\n", base64.StdEncoding.EncodeToString(protoKey)) + os.Exit(0) + } o, vpnOpts, ll := cliToOpts(c) // Egress and DHCP needs the Alive service diff --git a/cmd/util.go b/cmd/util.go index 58aa2622..2b854cd8 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -14,6 +14,7 @@ limitations under the License. package cmd import ( + "encoding/base64" "encoding/json" "fmt" "os" @@ -321,6 +322,11 @@ var CommonFlags []cli.Flag = []cli.Flag{ Usage: "Enable peerguard. (Experimental)", EnvVars: []string{"PEERGUARD"}, }, + &cli.StringFlag{ + Name: "privkey", + Usage: "Use fixed base64 <- protobuf encoded privkey. (Experimental)", + EnvVars: []string{"EDGEVPNPRIVKEY"}, + }, &cli.BoolFlag{ Name: "privkey-cache", Usage: "Enable privkey caching. (Experimental)", @@ -495,13 +501,19 @@ func cliToOpts(c *cli.Context) ([]node.Option, []vpn.Option, *logger.Logger) { } } - // Check if we have any privkey identity cached already - if c.Bool("privkey-cache") { + if c.String("privkey") != "" { + raw, err := base64.StdEncoding.DecodeString(c.String("privkey")) + if err != nil { + checkErr(fmt.Errorf("failed to decode privkey: %v", err)) + } else { + nc.Privkey = raw + } + // Check if we have any privkey identity cached already + } else if c.Bool("privkey-cache") { keyFile := filepath.Join(c.String("privkey-cache-dir"), "privkey") dat, err := os.ReadFile(keyFile) if err == nil && len(dat) > 0 { llger.Info("Reading key from", keyFile) - nc.Privkey = dat } else { // generate, write diff --git a/config.yml b/config.yml new file mode 100644 index 00000000..0343be5d --- /dev/null +++ b/config.yml @@ -0,0 +1,19 @@ +otp: + dht: + interval: 360 + key: YVMwYeeoIJ9yGQeuRNHY3wigEhDEisAPcbt0L30vQ3q + length: 43 + crypto: + interval: 360 + key: 0Yu91JT1WPmWtmmrFD5tWrSzeyhLUVFWyiEXzIezcyz + length: 43 +room: k9xRlX6brHxKMAWhGgvsuoiwq4fcNyNtA7JQivZBYm7 +rendezvous: tFMpGqtqtFHckVt62FCklh9xqjTi9upKWCmXNrNBCpL +mdns: NG8LRHaJTRnR0tbOGgr73oQTZqvKEn0kllXQr5SfKDs +max_message_size: 20971520 + +trusted_peer_ids: + - 12D3KooWQi1XDFy1Ntv5WXLYJWbuFy1zbXM3F6jc4DUJKtaoZPpC +protected_store_key: + - trustzone + - trustzoneAuth diff --git a/config_example.yml b/config_example.yml new file mode 100644 index 00000000..0343be5d --- /dev/null +++ b/config_example.yml @@ -0,0 +1,19 @@ +otp: + dht: + interval: 360 + key: YVMwYeeoIJ9yGQeuRNHY3wigEhDEisAPcbt0L30vQ3q + length: 43 + crypto: + interval: 360 + key: 0Yu91JT1WPmWtmmrFD5tWrSzeyhLUVFWyiEXzIezcyz + length: 43 +room: k9xRlX6brHxKMAWhGgvsuoiwq4fcNyNtA7JQivZBYm7 +rendezvous: tFMpGqtqtFHckVt62FCklh9xqjTi9upKWCmXNrNBCpL +mdns: NG8LRHaJTRnR0tbOGgr73oQTZqvKEn0kllXQr5SfKDs +max_message_size: 20971520 + +trusted_peer_ids: + - 12D3KooWQi1XDFy1Ntv5WXLYJWbuFy1zbXM3F6jc4DUJKtaoZPpC +protected_store_key: + - trustzone + - trustzoneAuth diff --git a/docs/content/en/docs/Concepts/Overview/peerguardian.md b/docs/content/en/docs/Concepts/Overview/peerguardian.md index f39ffd98..44c15d70 100644 --- a/docs/content/en/docs/Concepts/Overview/peerguardian.md +++ b/docs/content/en/docs/Concepts/Overview/peerguardian.md @@ -59,7 +59,7 @@ $ curl -X PUT 'http://localhost:8080/api/ledger/trustzoneAuth/ecdsa_1/LS0tLS1CRU Now the private key can be used while starting new nodes: ```bash -PEERGATE_AUTH="{ 'ecdsa' : { 'private_key': 'LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQkhUZnRSTVZSRmlvaWZrdllhZEE2NXVRQXlSZTJSZHM0MW1UTGZlNlRIT3FBTTdkZW9sak0KZXVPbTk2V0hacEpzNlJiVU1tL3BCWnZZcElSZ0UwZDJjdUdnQndZRks0RUVBQ09oZ1lrRGdZWUFCQUdVWStMNQptUzcvVWVoSjg0b3JieGo3ZmZUMHBYZ09MSzNZWEZLMWVrSTlEWnR6YnZWOUdwMHl6OTB3aVZxajdpMDFVRnhVCnRKbU1lWURIRzBTQkNuVWpDZ0FGT3ByUURpTXBFR2xYTmZ4LzIvdEVySDIzZDNwSytraFdJbUIza01QL2tRNEIKZzJmYnk2cXJpY1dHd3B4TXBXNWxKZVZXUGlkeWJmMSs0cVhPTWdQbmRnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=' } }" +PEERGATE_AUTH='{ "ecdsa" : { "private_key": "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1JSGNBZ0VCQkVJQkhUZnRSTVZSRmlvaWZrdllhZEE2NXVRQXlSZTJSZHM0MW1UTGZlNlRIT3FBTTdkZW9sak0KZXVPbTk2V0hacEpzNlJiVU1tL3BCWnZZcElSZ0UwZDJjdUdnQndZRks0RUVBQ09oZ1lrRGdZWUFCQUdVWStMNQptUzcvVWVoSjg0b3JieGo3ZmZUMHBYZ09MSzNZWEZLMWVrSTlEWnR6YnZWOUdwMHl6OTB3aVZxajdpMDFVRnhVCnRKbU1lWURIRzBTQkNuVWpDZ0FGT3ByUURpTXBFR2xYTmZ4LzIvdEVySDIzZDNwSytraFdJbUIza01QL2tRNEIKZzJmYnk2cXJpY1dHd3B4TXBXNWxKZVZXUGlkeWJmMSs0cVhPTWdQbmRnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=" } }' $ edgevpn --peerguardian --peergate ``` @@ -88,7 +88,7 @@ $ curl -X PUT 'http://localhost:8080/api/peergate/disable' To init a new Trusted network, start nodes with `--peergate-relaxed` and add the neccessary auth keys: ```bash -$ edgevpn --peerguardian --peergate --peergate-relaxed +$ edgevpn --peerguard --peergate --peergate-relaxed $ curl -X PUT 'http://localhost:8080/api/ledger/trustzoneAuth/keytype_1/XXX' ``` diff --git a/pkg/blockchain/ledger.go b/pkg/blockchain/ledger.go index d1b5b8b4..089f0e33 100644 --- a/pkg/blockchain/ledger.go +++ b/pkg/blockchain/ledger.go @@ -21,6 +21,8 @@ import ( "io" "io/ioutil" "log" + "maps" + "slices" "sync" "time" @@ -35,6 +37,10 @@ type Ledger struct { blockchain Store channel io.Writer + + skipVerify bool + trustedPeerIDS []string + protectedStoreKeys []string } type Store interface { @@ -59,6 +65,16 @@ func (l *Ledger) newGenesis() { l.blockchain.Add(genesisBlock) } +func (l *Ledger) SkipVerify() { + l.skipVerify = true +} +func (l *Ledger) SetTrustedPeerIDS(ids []string) { + l.trustedPeerIDS = ids +} +func (l *Ledger) SetProtectedStoreKeys(keys []string) { + l.protectedStoreKeys = keys +} + // Syncronizer starts a goroutine which // writes the blockchain to the periodically func (l *Ledger) Syncronizer(ctx context.Context, t time.Duration) { @@ -123,8 +139,17 @@ func (l *Ledger) Update(f *Ledger, h *hub.Message, c chan *hub.Message) (err err return } + if len(l.protectedStoreKeys) > 0 && !slices.Contains(l.trustedPeerIDS, h.SenderID) { + for _, key := range l.protectedStoreKeys { + if !maps.Equal(l.blockchain.Last().Storage[key], block.Storage[key]) { + err = errors.Wrapf(err, "unauthorized attempt to write to protected bucket: %s", key) + return + } + } + } + l.Lock() - if block.Index > l.blockchain.Len() { + if l.skipVerify || block.Index > l.blockchain.Len() { l.blockchain.Add(*block) } l.Unlock() @@ -350,12 +375,14 @@ func (l *Ledger) Index() int { func (l *Ledger) writeData(s map[string]map[string]Data) { newBlock := l.blockchain.Last().NewBlock(s) - if newBlock.IsValid(l.blockchain.Last()) { - l.Lock() - l.blockchain.Add(newBlock) - l.Unlock() + if !l.skipVerify && !newBlock.IsValid(l.blockchain.Last()) { + return } + l.Lock() + l.blockchain.Add(newBlock) + l.Unlock() + bytes, err := json.Marshal(l.blockchain.Last()) if err != nil { log.Println(err) diff --git a/pkg/config/config.go b/pkg/config/config.go index 1ae5bd7d..635d554f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -469,7 +469,7 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) { // Build up the authproviders for the peerguardian aps := []trustzone.AuthProvider{} for ap, providerOpts := range c.PeerGuard.AuthProviders { - a, err := authProvider(llger, ap, providerOpts) + a, err := AuthProvider(llger, ap, providerOpts) if err != nil { return opts, vpnOpts, fmt.Errorf("invalid authprovider: %w", err) } @@ -482,6 +482,7 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) { node.WithNetworkService( pg.UpdaterService(dur), pguardian.Challenger(dur, c.PeerGuard.Autocleanup), + pguardian.AutoTrust(dur), ), node.EnableGenericHub, node.GenericChannelHandlers(pguardian.ReceiveMessage), @@ -497,7 +498,7 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) { return opts, vpnOpts, nil } -func authProvider(ll log.StandardLogger, s string, opts map[string]interface{}) (trustzone.AuthProvider, error) { +func AuthProvider(ll log.StandardLogger, s string, opts map[string]interface{}) (trustzone.AuthProvider, error) { switch strings.ToLower(s) { case "ecdsa": pk, exists := opts["private_key"] diff --git a/pkg/node/config.go b/pkg/node/config.go index fce40dc6..244ffd2d 100644 --- a/pkg/node/config.go +++ b/pkg/node/config.go @@ -76,6 +76,9 @@ type Config struct { Sealer Sealer PeerGater Gater + + TrustedPeerIDS []string + ProtectedStoreKeys []string } type Gater interface { diff --git a/pkg/node/connection.go b/pkg/node/connection.go index d315c065..583ad9ce 100644 --- a/pkg/node/connection.go +++ b/pkg/node/connection.go @@ -20,6 +20,7 @@ import ( "io" mrand "math/rand" "net" + "slices" internalCrypto "github.com/mudler/edgevpn/pkg/crypto" @@ -253,6 +254,21 @@ func (e *Node) handleEvents(ctx context.Context, inputChannel chan *hub.Message, continue } + // If we have enabled trusted arbiter peers + if len(e.config.TrustedPeerIDS) > 0 && e.host.ID().String() != m.SenderID { + // If we are not the trusted one + if !slices.Contains(e.config.TrustedPeerIDS, e.host.ID().String()) { + // If incoming message is not from trusted one + if !slices.Contains(e.config.TrustedPeerIDS, m.SenderID) { + e.config.Logger.Warnf("%s gated room message from %s - not present in trusted peer IDS", e.host.ID(), m.SenderID) + continue + } else { + // If we a non-trusted peer, and we receive a meesage from the trusted one - disable peerGater + peerGater = false + } + } + } + if peerGater { if e.config.PeerGater != nil && e.config.PeerGater.Gate(e, peer.ID(m.SenderID)) { e.config.Logger.Warnf("gated message from %s", m.SenderID) diff --git a/pkg/node/node.go b/pkg/node/node.go index f33a6c3b..a0de0060 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -16,6 +16,7 @@ package node import ( "context" "fmt" + "slices" "sync" "time" @@ -123,6 +124,12 @@ func (e *Node) Start(ctx context.Context) error { return err } + if len(e.config.TrustedPeerIDS) > 0 && !slices.Contains(e.config.TrustedPeerIDS, e.host.ID().String()) { + ledger.SkipVerify() + } + ledger.SetTrustedPeerIDS(e.config.TrustedPeerIDS) + ledger.SetProtectedStoreKeys(e.config.ProtectedStoreKeys) + // Send periodically messages to the channel with our blockchain content ledger.Syncronizer(ctx, e.config.LedgerSyncronizationTime) diff --git a/pkg/node/options.go b/pkg/node/options.go index 44ae8521..cc6e5f91 100644 --- a/pkg/node/options.go +++ b/pkg/node/options.go @@ -259,6 +259,9 @@ type YAMLConnectionConfig struct { Rendezvous string `yaml:"rendezvous"` MDNS string `yaml:"mdns"` MaxMessageSize int `yaml:"max_message_size"` + + TrustedPeerIDS []string `yaml:"trusted_peer_ids"` + ProtectedStoreKeys []string `yaml:"protected_store_keys"` } // Base64 returns the base64 string representation of the connection @@ -301,6 +304,8 @@ func (y YAMLConnectionConfig) copy(mdns, dht bool, cfg *Config, d *discovery.DHT } cfg.SealKeyLength = y.OTP.Crypto.Length cfg.MaxMessageSize = y.MaxMessageSize + cfg.TrustedPeerIDS = y.TrustedPeerIDS + cfg.ProtectedStoreKeys = y.ProtectedStoreKeys } const defaultKeyLength = 43 diff --git a/pkg/trustzone/authprovider/ecdsa/provider.go b/pkg/trustzone/authprovider/ecdsa/provider.go index 380fd0e9..f1fa3ee3 100644 --- a/pkg/trustzone/authprovider/ecdsa/provider.go +++ b/pkg/trustzone/authprovider/ecdsa/provider.go @@ -45,13 +45,13 @@ func ECDSA521Provider(ll log.StandardLogger, privkey string) (*ECDSA521, error) // It cycles over all the Trusted zone Auth data ( providers options, not where senders ID are stored) // and detects any key with ecdsa prefix. Values are assumed to be string and parsed as pubkeys. // The pubkeys are then used to authenticate nodes and verify if any of the pubkeys validates the challenge. -func (e *ECDSA521) Authenticate(m *hub.Message, c chan *hub.Message, tzdata map[string]blockchain.Data) bool { +func (e *ECDSA521) Authenticate(m *hub.Message, c chan *hub.Message, tzdata map[string]blockchain.Data) (bool, string) { sigs, ok := m.Annotations["sigs"] if !ok { e.logger.Debug("No signature in message", m.Message, m.Annotations) - return false + return false, "" } e.logger.Debug("ECDSA auth Received", m) @@ -67,17 +67,17 @@ func (e *ECDSA521) Authenticate(m *hub.Message, c chan *hub.Message, tzdata map[ if len(pubKeys) == 0 { e.logger.Debug("ECDSA auth: No pubkeys to auth against") // no pubkeys to authenticate present in the ledger - return false + return false, "" } for _, pubkey := range pubKeys { // Try verifying the signature if err := verify([]byte(pubkey), []byte(fmt.Sprint(sigs)), bytes.NewBufferString(m.Message)); err == nil { e.logger.Debug("ECDSA auth: Signature verified") - return true + return true, pubkey } e.logger.Debug("ECDSA auth: Signature not verified") } - return false + return false, "" } // Challenger sends ECDSA521 challenges over the public channel if the current node is not in the trusted zone. @@ -93,6 +93,7 @@ func (e *ECDSA521) Challenger(inTrustZone bool, c node.Config, n *node.Node, b * msg := hub.NewMessage("challenge") msg.Annotations = make(map[string]interface{}) msg.Annotations["sigs"] = string(signature) + msg.SenderID = n.Host().ID().String() n.PublishMessage(msg) return } diff --git a/pkg/trustzone/peerguardian.go b/pkg/trustzone/peerguardian.go index 001468ea..b7e870d8 100644 --- a/pkg/trustzone/peerguardian.go +++ b/pkg/trustzone/peerguardian.go @@ -39,9 +39,9 @@ func NewPeerGuardian(logger log.StandardLogger, authProviders ...AuthProvider) * // ReceiveMessage is a GenericHandler for public channel to provide authentication. // We receive messages here and we select them based on 2 criterias: -// - messages that are supposed to generate challenges for auth mechanisms. -// Auth mechanisms should get user auth data from a special TZ dedicated to hashes that are manually added -// - messages that are answers to such challenges and then means that the sender.ID should be added to the trust zone +// - messages that are supposed to generate challenges for auth mechanisms. +// Auth mechanisms should get user auth data from a special TZ dedicated to hashes that are manually added +// - messages that are answers to such challenges and then means that the sender.ID should be added to the trust zone func (pg *PeerGuardian) ReceiveMessage(l *blockchain.Ledger, m *hub.Message, c chan *hub.Message) error { pg.logger.Debug("Peerguardian received message from", m.SenderID) @@ -49,13 +49,15 @@ func (pg *PeerGuardian) ReceiveMessage(l *blockchain.Ledger, m *hub.Message, c c _, exists := l.GetKey(protocol.TrustZoneKey, m.SenderID) trustAuth := l.CurrentData()[protocol.TrustZoneAuthKey] - if !exists && a.Authenticate(m, c, trustAuth) { - // try to authenticate it - // Note we can also not be in a TZ here as we are not able to check (we miss node information at hand) - // In any way nodes would ignore the messages, and that we hit Authenticate is useful for two (or more) - // steps authenticators. - l.Persist(context.Background(), 5*time.Second, 120*time.Second, protocol.TrustZoneKey, m.SenderID, "") - return nil + if !exists { + if ok, pubkey := a.Authenticate(m, c, trustAuth); ok { + // try to authenticate it + // Note we can also not be in a TZ here as we are not able to check (we miss node information at hand) + // In any way nodes would ignore the messages, and that we hit Authenticate is useful for two (or more) + // steps authenticators. + l.Persist(context.Background(), 5*time.Second, 120*time.Second, protocol.TrustZoneKey, m.SenderID, pubkey) + return nil + } } } @@ -73,22 +75,56 @@ func (pg *PeerGuardian) Challenger(duration time.Duration, autocleanup bool) nod a.Challenger(exists, c, n, b, trustAuth) } - // Automatically cleanup TZ from peers not anymore in the hub + // Automatically cleanup TZ from peers not anymore in the hub, or when the public key was invalidated if autocleanup { peers, err := n.MessageHub.ListPeers() if err != nil { return } + // Append ourselves, since trustzone needs to be consistent across every peer + peers = append(peers, n.Host().ID()) + tz := b.CurrentData()[protocol.TrustZoneKey] - for k, _ := range tz { - PEER: + for peer, peerPubkey := range tz { + // Test if trustzone peer still in the hub + peerFound := false for _, p := range peers { - if p.String() == k { - break PEER + if p.String() == peer { + peerFound = true + break + } + } + if !peerFound { + b.Delete(protocol.TrustZoneKey, peer) + continue + } + + // Skip key validation for initially trusted peers, + // as they don't have to share their key + peerTrusted := false + for _, p := range c.TrustedPeerIDS { + if p == peer { + peerTrusted = true + break } } - b.Delete(protocol.TrustZoneKey, k) + if peerTrusted { + continue + } + + // Test if peer public key was invalidated + keyFound := false + for _, pubkey := range trustAuth { + if pubkey == peerPubkey { + keyFound = true + break + } + } + if !keyFound { + b.Delete(protocol.TrustZoneKey, peer) + continue + } } } }) @@ -96,10 +132,34 @@ func (pg *PeerGuardian) Challenger(duration time.Duration, autocleanup bool) nod } } +// AutoTrust is a NetworkService that should add self peer ID to the trustzone if it exists in initial trusted peer IDs list +func (pg *PeerGuardian) AutoTrust(duration time.Duration) node.NetworkService { + return func(ctx context.Context, c node.Config, n *node.Node, b *blockchain.Ledger) error { + + selfTrust := false + for _, p := range c.TrustedPeerIDS { + if p == n.Host().ID().String() { + selfTrust = true + break + } + } + if !selfTrust { + return nil + } + + b.Announce(ctx, duration, func() { + if _, exists := b.GetKey(protocol.TrustZoneKey, n.Host().ID().String()); !exists { + b.Persist(context.Background(), 5*time.Second, 120*time.Second, protocol.TrustZoneKey, n.Host().ID().String(), "") + } + }) + return nil + } +} + // AuthProvider is a generic Blockchain authentity provider type AuthProvider interface { // Authenticate either generates challanges to pick up later or authenticates a node // from a message with the available auth data in the blockchain - Authenticate(*hub.Message, chan *hub.Message, map[string]blockchain.Data) bool + Authenticate(*hub.Message, chan *hub.Message, map[string]blockchain.Data) (bool, string) Challenger(inTrustZone bool, c node.Config, n *node.Node, b *blockchain.Ledger, trustData map[string]blockchain.Data) } diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..23d00f31 --- /dev/null +++ b/test.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +main_keys=($(go run main.go peergater ecdsa-genkey | cut -d: -f2- )) +client_keys=($(go run main.go peergater ecdsa-genkey | cut -d: -f2- )) +MAIN_PRIVKEY=${main_keys[0]} +MAIN_PUBKEY=${main_keys[1]} +CLIENT_PRIVKEY=${client_keys[0]} +CLIENT_PUBKEY=${client_keys[1]} + +# export EDGEVPNDHTANNOUNCEMADDRS=/ip4/.../tcp/.../p2p/... +export EDGEVPNCONFIG=config.yml +export EDGEVPNPEERGATEINTERVAL=10 +export EDGEVPNPRIVKEY='CAESQOV82ydHYcTFqyjf6fE6Zrdr9aH97GwGODEWm9HmELv73T55KPBrW5n3D29Df7b+DjH1zVzqUa1cgpTBHiEBdgk=' +export PEERGATE=true +export PEERGUARD=true +export PEERGATE_AUTOCLEAN=true +export PEERGATE_AUTH='{ "ecdsa" : { "private_key": "'$MAIN_PRIVKEY'" } }' + +# killall main is a bad idea, but that worked on my machine +sudo -E bash -c " + IFACE=\"utun10\" go run main.go api --enable-healthchecks & + sleep 3 + + curl -X PUT http://127.0.0.1:8080/api/ledger/trustzoneAuth/ecdsa_client/"$CLIENT_PUBKEY" + + export -n EDGEVPNPRIVKEY + export -n PEERGATE + export PEERGATE_AUTH='{ \"ecdsa\" : { \"private_key\": \""$CLIENT_PRIVKEY"\" } }' + + IFACE=\"utun11\" go run main.go + killall main +"