# \[H1\] ghost-eye stores sr25519 seeds in plaintext on disk — wallet-keys, address-book, and node stash keys world-readable

**URL:** https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124
**Category:** Submitted
**Tags:** ghost-eye
**Created:** [October 3, 2026, 12:27am UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124 "2026-10-03T00:27:39Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![Chaplin](https://ghostforum.org/letter_avatar_proxy/v4/letter/c/74df32/32.png) [@Chaplin](https://ghostforum.org/u/Chaplin)
#### Post date: [October 3, 2026, 12:27am UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124/1 "2026-10-03T00:27:40Z")

</div>

## Summary

`ghost-eye` (the GHOST operator TUI) persists every secret it manages as plaintext  
`name:0x<64-hex-seed>` lines, with no encryption, no password, and no restrictive file  
permissions:

| Secret | File | Writer |
| --- | --- | --- |
| Wallet seeds (all accounts) | `~/.local/share/ghost-eye/wallet-keys` | `accounts.rs:230` `save_to_file()` |
| Address-book entries (same `name:0xseed` format) | `~/.local/share/ghost-eye/address-book` | `address_book.rs:68` `save_to_file()` |
| Freshly generated stash key | `/etc/ghost/stash-key` | `stash_info.rs:134-135` `generate_and_save_new_key()` |

All writes use `File::create` with default permissions (0666 & !umask — verified empirically;  
under the standard default umask 022 this is **0644 world-readable** ). The data directory  
itself is created at startup by `logging::init()` (`src/logging.rs:13-14`) via  
`create_dir_all` (0755 default), so the whole tree is world-readable/traversable on typical  
systems. The codebase has no `mode(0o600)`/`set_permissions` call, no password prompt, and  
no encryption crate in `Cargo.toml`.

Worse, the TUI actively funnels the node’s most sensitive keys into the plaintext file:  
pressing `L` (`load_initial_keys`, `accounts.rs:399`) — and the automatic first-run import  
(`read_or_create`, `accounts.rs:246`) — copies `/etc/ghost/wallet-key` and  
`/etc/ghost/stash-key` (the **validator’s session/stash keys** , controlling staked funds)  
into `wallet-keys` through the same `save_to_file()`. Seeds also live in process memory as  
`String`s (`AccountInfo.seed`), cloned through mpsc channels with no zeroization. Any local  
user, backup/disk-image reader, or co-tenant process recovers full sr25519 seeds — complete  
signing control over operator, stash and wallet funds.

## Severity

**High — CVSS 3.1: 7.8** — `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`

Local attack vector (world-readable file under default umask), no privileges beyond any  
local account, no user interaction. Full confidentiality/integrity/availability impact on  
key material: seed compromise = total fund control.

## Affected code

```rust
// src/components/wallet/accounts.rs:228-233
fn save_to_file(&mut self) {
    let mut file = File::create(&self.wallet_keys_file).unwrap(); // 0666 & !umask
    for wallet in self.wallet_keys.iter() {
        writeln!(file, "{}:0x{}", wallet.name, &wallet.seed).unwrap(); // plaintext seed
    }
}

```

```rust
// src/components/wallet/accounts.rs:399-402 — 'L' key pulls node keys into the file
fn load_initial_keys(&mut self) {
    self.try_load_and_store_keys("/etc/ghost/wallet-key", "ghostie");
    self.try_load_and_store_keys("/etc/ghost/stash-key", "stash");
}

```

```rust
// src/components/validator/stash_info.rs:126-135
fn generate_and_save_new_key(&mut self) -> Result<()> {
    let (pair, seed) = Pair::generate();
    let secret_seed = hex::encode(seed);
    ...
    let mut new_file = File::create(&self.stash_filepath)?; // /etc/ghost/stash-key
    writeln!(new_file, "0x{}", &secret_seed)?; // plaintext

```

```rust
// src/components/wallet/address_book.rs:67-72 — same pattern, second file
fn save_to_file(&mut self) {
    let mut file = File::create(&self.address_book_file).unwrap();
    for wallet in self.address_book.iter() {
        writeln!(file, "{}:0x{}", wallet.name, &wallet.seed).unwrap();
    }
}

```

Data dir: `ProjectDirs::from("com", "ghost", "ghost-eye")` (`src/config.rs:124-126`) →  
`~/.local/share/ghost-eye/` on Linux.

## Steps to reproduce

1. `git clone https://git.ghostchain.io/ghostchain/ghost-eye && cd ghost-eye`
2. `git rev-parse HEAD` → `ee23e6ba643148edd11944fae2d294daf017a18a`
3. Build and run (`cargo run`), open the wallet tab, create an account (or press `L` with  
`/etc/ghost/wallet-key` present).
4. `cat ~/.local/share/ghost-eye/wallet-keys` → `mywallet:0x9d61b19d…` (64 hex chars = raw  
sr25519 seed); `ls -l` → `-rw-r--r--`.
5. `Pair::from_seed(&hex::decode(seed))` reconstructs the full signing key — the app itself  
does exactly this to sign (`accounts.rs:254`, `predefined_txs.rs:347`).

The write path is unconditional: create/rename/delete/import all call `save_to_file()`.

## Impact

- **Direct key exposure:** any local user or process with filesystem read access obtains  
complete sr25519 seeds for operator wallets and validator stash/session keys — full  
authority over funds and staking operations. Backups, disk images, container layers and  
monitoring agents that can read the file are all key-compromise vectors.
- **Privilege boundary crossed:** the `L`-key/first-run import moves keys from root-owned  
`/etc/ghost/*` into a user-readable `$HOME` file, widening exposure beyond the node  
operator’s own hardening.
- **No defense in depth:** no password, no OS keyring, no file-mode hardening, no  
in-memory zeroization.

## Remediation

- Encrypt at rest: password prompt on first run, Argon2id + ChaCha20-Poly1305; never store  
plaintext seeds.
- Short term: create key files with `0o600`, verify parent dir permissions, and stop  
importing `/etc/ghost/*` node keys into the wallet file (or warn loudly).
- Zeroize seed material in memory (`zeroize` crate); load seeds on demand for signing  
instead of keeping them resident as `String`s.
- Parse the wallet-keys file defensively — currently a blank/malformed line panics the TUI  
on startup (`&""[3..]`, `accounts.rs:254`).

---

<div class="post-metadata">

### Author: ![VoidDragon](https://ghostforum.org/user_avatar/ghostforum.org/voiddragon/32/16_2.png) [@VoidDragon](https://ghostforum.org/u/VoidDragon)
#### Post date: [October 3, 2026, 1:30pm UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124/2 "2026-10-03T13:30:03Z")

</div>

Many thanks for your report!

IMHO all of this should be taken care of by the OS account managment and is entirely the responsibility of the user who configured the system. But based on what you are saying you are recommending file permissions to be `0600` for `wallet-keys` file?

For now I do not think this is a big issue, but you may be able to argue for a higher severity if you can continue with more in-depth information. Can you estimate the risks of it? What should attacker do for abusing that: get access to the machine (1), have ability to escalate privileges (2), or any other (3..)

---

<div class="post-metadata">

### Author: ![Chaplin](https://ghostforum.org/letter_avatar_proxy/v4/letter/c/74df32/32.png) [@Chaplin](https://ghostforum.org/u/Chaplin)
#### Post date: [October 3, 2026, 3:31pm UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124/3 "2026-10-03T15:31:07Z")

</div>

Thanks for engaging with this. Let me address both points directly.

**1. Yes — 0600 is the recommendation, and it is the industry standard, not a nice-to-have.**

Every serious key-handling software sets restrictive permissions by default: OpenSSH refuses to use keys readable by others, Geth writes keystore files at 0600, Bitcoin Core does the same for wallets. The reason is defense in depth: key material must never depend _solely_ on the operator remembering to `chmod` files the application itself created. Secure defaults are the application’s responsibility — a user running your TUI should not need to audit file permissions afterward to stay safe.

**2. Threat model — no privilege escalation needed. That is the point.**

The files are created **0644 world-readable** (0666 & !umask under the standard umask 022), in a **0755 world-traversable** directory. The attacker does _not_ need root and does _not_ need to escalate privileges. Any of these suffices:

- **A compromised low-privilege service on the same host.** Validators commonly run monitoring agents, exporters, log shippers, or web dashboards as separate users. If any one of those is compromised (a very common initial-foothold scenario), the attacker reads the validator’s stash seed as an unprivileged user — no privesc required — and gains full signing control over staked funds.
- **Container / shared environments.** A sidecar container, a CI runner, or any co-tenant process with host filesystem visibility reads the keys.
- **Backups, snapshots, disk images.** World-readable files end up in backups and images that are handled with weaker access controls than the live machine.
- **The `L`-key flow widens this further:** pressing `L` (or first-run import) copies `/etc/ghost/wallet-key` and `/etc/ghost/stash-key` — the validator’s session and stash keys — into the same world-readable file. So even an operator who carefully protected `/etc/ghost/*` gets those keys silently duplicated into a world-readable location by the TUI.

So the answer to your question is: the attacker needs **local unprivileged access only** (scenario 1 covers it — no escalation), which is a realistic and common foothold. The impact is total: full sr25519 seeds mean complete signing control over operator, stash, and wallet funds.

The fix is small (set 0600 on creation, ideally with an explicit `mode()` call rather than relying on umask), which is why I scored it High rather than Critical — but “the OS should handle it” does not hold when the application is the one creating world-readable key files by default.

---

<div class="post-metadata">

### Author: ![VoidDragon](https://ghostforum.org/user_avatar/ghostforum.org/voiddragon/32/16_2.png) [@VoidDragon](https://ghostforum.org/u/VoidDragon)
#### Post date: [October 5, 2026, 9:27am UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124/4 "2026-10-05T09:27:33Z")

</div>

Actually, you are right - but I doubt that this would be an issue for all participants. From what I understand, an attacker would need some form of access to the machine (not sudo, as you mentioned), which is why I see this as more a matter of the computer’s security than of the software.

In any case, because the fix is super easy, it could be done - but its priority is low, because we’re relying on the premise that getting access to the machine is extremely difficult. What are your thoughts on this?

---

<div class="post-metadata">

### Author: ![Chaplin](https://ghostforum.org/letter_avatar_proxy/v4/letter/c/74df32/32.png) [@Chaplin](https://ghostforum.org/u/Chaplin)
#### Post date: [October 5, 2026, 11:29am UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124/5 "2026-10-05T11:29:47Z")

</div>

I agree that this requires local access and would not affect every participant.

The required access is unprivileged local read access, not root access or privilege escalation. Once another local user or process can read the application data directory, the files are accessible because ghost-eye creates them with default 0644 permissions inside a 0755 directory. No additional escalation is needed to recover the raw sr25519 seed.

This is also not only a case of an operator forgetting to restrict an existing sensitive file. The application creates the key material with overly broad permissions by default. The L / first-run import path also copies /etc/ghost/wallet-key and /etc/ghost/stash-key into wallet-keys, so keys that were initially stored in a more restricted location can be duplicated into the application data directory with those permissions.

I agree that obtaining local access may be less likely than exploiting a remote vulnerability, and I am not claiming that every validator is exposed in practice. My point is that once that local foothold exists, no sudo, privilege escalation, or user interaction is required. Reading the seed is enough to obtain the corresponding signing authority.

So I understand if the local-access prerequisite leads to a lower severity assessment. Technically, though, I still consider this a software-side security issue rather than something that should be left entirely to OS configuration. The application should create these files with restrictive permissions from the start.
