Environment variables security on a frontend casniter

Hello everyone, I have a frontend(asset) canister which requires some private api keys to run. at first i was thinking of putting them into .env file however the problem with that is, the keys are exposed in the project’s chunk files. Is there any way to specify some private keys in the canister? or do you have a better solution to make those keys completely private

Storing secrets that need to be accessed at runtime is still unsolved. If you store them in encrypted fashion and only decrypt off-chain then it would be safe, but not all applications work with that model

3 Likes

Most apis mention in their documentation where and how their secret keys should be used and stored. Some secrets are fine to use in the frontend or e.g. a mobile application, some secrets should only be used and stored within a backend.

One difference here between web2 and the IC here is that canister state is private to a certain degree, it’s technically at risk of malicious subnet nodes inspecting the canister state. On the other hand on web2, your e.g. VPS provider could technically also be malicious in a similar fashion.

In this regard, for higher levels of security one could look into more advanced concepts like Threshold ECDSA, canister signatures and VetKeys.

Overal, storing secrets is a case by case evaluation, the api documentation that provides these secrets should be the primary guide in this regard.

2 Likes

How Can a Node Provider Access State Data?

This concern is occasionally mentioned in forums, but it is rarely explained in detail. Understanding this potential risk is crucial for developers.

One significant factor to consider is the absence of a formal legal contract between developers and node providers. As developers, we rely on these providers, but the lack of contractual safeguards raises questions about the security and confidentiality of state data.

Can someone clarify this issue? How is state data handled, and what measures are in place to prevent unauthorized access by node providers?

1 Like

Thats true, however in my case I have a private key that my frontend requires to call my backend canister. This way I can make sure that the call is only coming from the frontend if the key is correct since no one else should have access to that key.
The case with threshold esdca will not help in this case since I can’t make a call to the management canister through my asset canister. not to mention that for each signature I have to pay at least 25 billion cycles considering the number of calls I need to make in a single day, the cost will skyrocket if I use threshold ecdsa.

See the followed thread and it’s linked threads that go into more detail and ongoing developments: AMD SEV Virtual Machine Support

Assuming this private key is not sent “as is” but used to sign the requests to the IC and the backend checks the caller principal as security check.

Then, if I understand correctly, your concern is with the risk of a possible extraction of this private key from the users frontend e.g. XSS, malicious software etc.

There are different ways to sign calls, each way has different security assumptions:

  1. ED25519 keypair: could be extracted as mentioned above
  2. ECDSA keypair: can’t be extracted directly if marked as non extractable. Sadly malicious code could still sign a delegation and “kind of” extract it this way.
  3. Delegation with expiry, for example Internet Identity its delegation could be extracted but it would expire at some point.
  4. WebAuthn/Passkey: can’t be extracted, but does require user interaction e.g. scan fingerprint.
  5. Wallet: private key security implementation is up to the wallet, requires user interaction where the user can see what it’s signing.
  6. HW wallet: airgapped, private key never leaves HW wallet, requires user interaction where the user can see what it’s signing.

The first two options are great for calls that e.g. get your user profile from a social dapp. The third option is a step up in security which is great for e.g. sign in, confirmation of account deletion etc. The last two options are commonly used on DeFI, where you don’t just want to approve an action but also want to see what action you’re approving e.g. send 10 ICP.

Keep in mind, security could also be a hybrid design that e.g. uses option 5 to register a principal of option 2 in the canister. Where this registration could be valid for a limited time window and/or use.

1 Like

Regarding your reply if I try to use ecdsa signing, is there any way to use that in a frontend(asset) canister?
I’m using threshold ecdsa api in my rust canister, but how can
I implement the same thing in a frontend canister?

And I can’t use a wallet since i need to save the private key of that wallet somewhere and that can be exposed as well, it’s basically the same story

Threshold ECDSA signing is meant for use within a canister, it’s out of the scope of a frontend key pair :sweat_smile:

That depends on the wallet, if the wallet is a browser extension, native desktop application, airgapped on another device etc it would be saved outside the scope of the browser web environment.

With which key pair the canister calls are signed in your dapp, really depends on your use case. Without further details regarding the dapp use case, all I can do is show the available options as shown above.

So from what I understood there is no secure way of making a call to my rust canister from my frontend and guarantee that the call is only coming from the front end canister not somewhere else.
I could potentially use some off chain components like a node server but I would prefer to not use sth like that due to security concerns.
Now I only have one question, Is dfinity team working on a solution that allows to add some environment variables to asset canisters? cause that would solve a lot of problems.

As I understand it, this is about a specific FE canister calling a specific BE canister; and said BE canister only allowing calls from this FE canister (and possibly others like it), as opposed to anyone.

If so, you can let the BE canister know about the specific FE canister ID(s) and, on most endpoints, refuse to respond to calls originating anywhere else, whether other canisters or ingress messages. There exists an API to query the identity of the caller (I forget which) and there is no way to spoof that.

In other words, you can put the burden of verifying the identity of the caller (FE) on the callee (BE). Then you won’t need any shared secret.

Yes that’s completely correct, I just couldn’t find how to make a call to my (BE) canister through my (FE) canister in way that ic_cdk::caller(); will be my (FE) canister id.
Is that even possible?

If your FE calls your BE, the caller will be the FE’s canister ID, regardless of how that call was triggered (whether it was an ingress message, another canister call or a heartbeat). There’s nothing you need to do about it, the output is literally “who sent this canister request / ingress message”.

Right now I’m making calls to my BE canister through either anonymous identity or users connected wallet principal. but the thing I couldn’t find is how can I make a call from my FE canister using agent_js in a way that the caller will be my FE principal id. I could not find anything for that.

Not to forget It’s an asset canister and I’m using typescript. Therefor I don’t have the same flexibility as a motoko or a rust canister.

You would have to call your FE canister and have it call your BE canister (browser → FE → BE). What you seem to have is your FE canister serving some HTML and JS assets over HTTP (browser → FE) and then later the browser calling the BE directly via the JS agent (browser → BE). Which is not the same.

Do note that with this setup it would be unsafe to rely on an API key, as your FE canister would have to hand over the API key to the browser so that it could then call the BE directly.

Yes thats completely unsafe to rely on api keys, therefore I started this post.
But regarding your reply what I couldn’t understand is, how to make a call to an asset canister(FE) from browser, and then make another call from (FE) to (BE) canister. (FE) is just a simple typescript next js canister, it’s not like rust or motoko that allows us to define update and query endpoints.
Do you have any code example of what you just described?
Browser → FE → BE

Why not store the API key on the backend? You could design a process like this:

1.	The client calls the backend canister (BC).
2.	The BC performs an HTTP outcall to the Web2 service using the stored API key.
3.	The BC receives the response from the Web2 service.
4.	The BC then delivers the result back to the browser.

This approach will be slower, but you have more control over your api keys.