One key per job: four credentials in one system, so one leak opens nothing else
The sending machine, the owner, the public visitor and the follower each get their own key: bound API keys, session cookies and public links, from a live system.
- Separate credentials by who uses them and what for. Each key does only its job, and one leak opens no other door
- A good API key has a recognisable prefix, comes from a secure random source, and is bound to what it may write
- Session cookies need HttpOnly, Secure and SameSite to limit the damage from XSS and CSRF
Our trading journal has four kinds of visitor: the trading program sending data every few seconds, the account owner logging into the dashboard, anyone opening a public results link, and followers pulling trades to copy. If they all used the same kind of key, the key embedded in a program on someone else’s machine would open every door. This article walks through how the four are kept apart and why, written from the side of designing a system to be safe.
The principle: one key, one job
Think of an office building. The courier’s card opens only the delivery room. The cleaner gets into every room but not the safe. Visitors sit in the lobby. Lose one card, and only that card is cancelled.
Principle: separate keys by who uses them and what for. Each key does only its job.
| Who | Uses | Can | Cannot |
|---|---|---|---|
| Trading program | API key in the X-Api-Key header | send one account’s data | read data, open the dashboard |
| Account owner | email + password → session cookie | view and manage their own accounts | write trading data in place of the program |
| Anyone | public link /p/<slug> | view statistics, read-only | see the full account number, change anything |
| Follower | a share token in X-Copy-Token | read the positions being held | send data, open the dashboard |
Key 1: the API key for the machine that sends data
Our keys look like this:
fxk_Q7mZp2Kx9vLr4Tn8Wc1Yh5Bd6Sa3
A prefix. fxk_ makes a key recognisable the moment it appears in a log, a config file, or code accidentally committed to git. Secret scanners can match it with a simple rule.
A secure random source. Go’s crypto/rand, not math/rand, whose next value can be predicted. The random part is 28 characters long.
func IngestKey() string { return "fxk_" + randString(28) } // randString uses crypto/rand
Bound to one trading account. This matters most. The first time the program sends data, the system binds the key to the trading account number in that data. From then on, if data for a different account arrives with the same key, the system answers 409 Conflict and stores nothing.
if payloadLogin != 0 && payloadLogin != acct.Login {
writeError(w, http.StatusConflict,
"account login mismatch: this key belongs to a different MT account")
return false
}
Why bind it? Because this is a trading verification system. If one account’s key could mix another account’s data in, the results shown could not be trusted, whether someone meant to cheat or just copied a config file to another account and forgot to change the key.
Key 2: the session of a logged-in person
People log in with an email and password. Passwords are stored as bcrypt hashes, never in plain form:
hash, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
ok := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil
After a successful login, the system creates a random 40-character session token, stores it on the server, and gives it to the browser as a cookie set like this:
http.SetCookie(w, &http.Cookie{
Name: "fxsess", Value: token, Path: "/",
HttpOnly: true, // JavaScript cannot read it
Secure: secure, // sent over HTTPS only
SameSite: http.SameSiteLaxMode, // not attached to cross-site POSTs
MaxAge: 30 * 24 * 60 * 60, // 30 days
})
- HttpOnly: if the page ever has an XSS hole, the injected script still cannot read this cookie
- Secure: the cookie never travels over plain HTTP, where it could be sniffed
- SameSite=Lax: another site cannot submit a form that acts as the user, reducing CSRF risk
- Logging out deletes the session on the server, not just the cookie in the browser, so a token stolen earlier stops working
Key 3: the public link
An owner can choose to publish a public page. It uses a random 10-character slug such as /p/k3Xm9QaZ2w, not an account number or a sequential id, so nobody can walk through other people’s pages by guessing.
The page is read-only and hides the trading account number, showing only the last four digits:
func maskLogin(login int64) string {
s := strconv.FormatInt(login, 10)
return "••••" + s[len(s)-4:]
}
The full account number is not needed to judge results, and could be used to make a scam look convincing, so it is not shown.
Key 4: the share token for followers
Followers need no account at all, just a token the owner hands them. The token is read-only, and the owner can rotate it at any time to cut every follower off at once. The system deletes the old token’s cache immediately instead of waiting for it to expire (the details are in the article on the hot path cache).
A checklist for good keys
- Separated by job: no key can do everything
- Prefixed, so it is recognised when it ends up in the wrong place
- From a secure random source, and long enough not to be guessed
- Bound to a scope: one account, one permission
- Rotatable without affecting other keys
- Stored as a hash only for keys used to authenticate, with the full key shown once at creation
- Sent in a header, not in the URL, because URLs end up in server and proxy logs
- Rate-limited, against guessing
Common mistakes
- Embedding a person’s password in a program or an automated script
- Generating tokens with
Math.random()ormath/rand - Leaving HttpOnly off because the page “wants to read” the token itself
- Logging out by deleting the cookie in the browser while the server-side session still works
- Using sequential ids for public links, so anyone can change the number and see someone else’s
FAQ
Why not let the sending machine log in with an email and password like a person?
Because the password would sit inside a program on another machine. If it leaked, the holder could open the whole dashboard. A separate API key is limited to sending one account's data, and can be revoked without changing any password.
How should API keys be stored in the database?
The good practice is to store only a hash of the key, as with passwords, and show the full key to the user once when it is created. If the database leaks, nobody gets a working key from it.
Commands and settings were tested on sample systems. Try them on a test machine before production.