Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add encryption for sensitive data and configure environment variables #245

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.exemple
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ENCRYPTION_KEY="aba3aa8e29b9904d5d8d705230b664c053415c54be20ad13be99af0057dfa23a" // Random key generated by crypto.randomBytes(32).toString("hex")
SERVER_PORT=6989
NODE_ENV=development
82 changes: 70 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,94 @@

## πŸ€” What is Nexterm?

The open source server management software for SSH, VNC & RDP
Nexterm is an open-source server management software that allows you to:

- Connect remotely via SSH, VNC and RDP
- Manage files through SFTP
- Deploy applications via Docker
- Manage Proxmox LXC and QEMU containers
- Secure access with two-factor authentication

## πŸš€ Run preview

You can run a preview of Nexterm by clicking [here](https://docs.nexterm.dev/preview).

## πŸ’» Development

### Prerequisites

- Node.js 18+
- Yarn
- Docker (optional)

### Local Setup

#### Clone the repository

```sh
git clone https://github.com/gnmyt/Nexterm.git
cd Nexterm
```

#### Install dependencies

```sh
yarn install
cd client && yarn install
cd ..
```

#### Start development mode

```sh
yarn dev
```

## πŸ”§ Configuration

The server listens on port 6989 by default. You can modify this behavior using environment variables:

- `SERVER_PORT`: Server listening port (default: 6989)
- `NODE_ENV`: Runtime environment (development/production)
- `ENCRYPTION_KEY`: Encryption key for passwords, SSH keys and passphrases (default: Randomly generated key)

## πŸ›‘οΈ Security

- Two-factor authentication
- Session management
- Password encryption
- Docker container isolation

## 🀝 Contributing

Contributions are welcome! Please feel free to:

1. Fork the project
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request

## πŸ”— Useful Links

- [Documentation](https://docs.nexterm.dev)
- [Discord](https://dc.gnmyt.dev)
- [Report a bug](https://github.com/gnmyt/Nexterm/issues)
- [Request a feature](https://github.com/gnmyt/Nexterm/issues)

## License

Distributed under the MIT license. See `LICENSE` for more information.

[contributors-shield]: https://img.shields.io/github/contributors/gnmyt/Nexterm.svg?style=for-the-badge

[contributors-url]: https://github.com/gnmyt/Nexterm/graphs/contributors

[forks-shield]: https://img.shields.io/github/forks/gnmyt/Nexterm.svg?style=for-the-badge

[forks-url]: https://github.com/gnmyt/Nexterm/network/members

[stars-shield]: https://img.shields.io/github/stars/gnmyt/Nexterm.svg?style=for-the-badge

[stars-url]: https://github.com/gnmyt/Nexterm/stargazers

[issues-shield]: https://img.shields.io/github/issues/gnmyt/Nexterm.svg?style=for-the-badge

[issues-url]: https://github.com/gnmyt/Nexterm/issues

[license-shield]: https://img.shields.io/github/license/gnmyt/Nexterm.svg?style=for-the-badge

[license-url]: https://github.com/gnmyt/Nexterm/blob/master/LICENSE

[release-shield]: https://img.shields.io/github/v/release/gnmyt/Nexterm.svg?style=for-the-badge

[release-url]: https://github.com/gnmyt/Nexterm/releases/latest
53 changes: 36 additions & 17 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
require("dotenv").config();
const express = require("express");
const path = require("path");
const db = require("./utils/database");
const { authenticate } = require("./middlewares/auth");
const expressWs = require("express-ws");
const { startPVEUpdater } = require("./utils/pveUpdater");
const { refreshAppSources, startAppUpdater, insertOfficialSource } = require("./controllers/appSource");
const {
refreshAppSources,
startAppUpdater,
insertOfficialSource,
} = require("./controllers/appSource");
const { isAdmin } = require("./middlewares/permission");
require("./utils/folder");

process.on("uncaughtException", err => require("./utils/errorHandling")(err));
process.on("uncaughtException", (err) => require("./utils/errorHandling")(err));

const APP_PORT = process.env.SERVER_PORT || 6989;

Expand Down Expand Up @@ -42,34 +47,48 @@ app.use("/api/apps", authenticate, require("./routes/apps"));
if (process.env.NODE_ENV === "production") {
app.use(express.static(path.join(__dirname, "../dist")));

app.get("*", (req, res) => res.sendFile(path.join(__dirname, "../dist", "index.html")));
app.get("*", (req, res) =>
res.sendFile(path.join(__dirname, "../dist", "index.html"))
);
} else {
app.get("*", (req, res) => res.status(500).sendFile(path.join(__dirname, "templates", "env.html")));
app.get("*", (req, res) =>
res.status(500).sendFile(path.join(__dirname, "templates", "env.html"))
);
}

db.authenticate().catch(err => {
console.error("Could not open the database file. Maybe it is damaged?: " + err.message);
process.exit(111);
}).then(async () => {
console.log("Successfully connected to the database " + (process.env.DB_TYPE === "mysql" ? "server" : "file"));
db.authenticate()
.catch((err) => {
console.error(
"Could not open the database file. Maybe it is damaged?: " +
err.message
);
process.exit(111);
})
.then(async () => {
console.log(
"Successfully connected to the database " +
(process.env.DB_TYPE === "mysql" ? "server" : "file")
);

await db.sync({ alter: true, force: false });
await db.sync({ alter: true, force: false });

startPVEUpdater();
startPVEUpdater();

startAppUpdater();
startAppUpdater();

await insertOfficialSource();
await insertOfficialSource();

await refreshAppSources();
await refreshAppSources();

app.listen(APP_PORT, () => console.log(`Server listening on port ${APP_PORT}`));
});
app.listen(APP_PORT, () =>
console.log(`Server listening on port ${APP_PORT}`)
);
});

process.on("SIGINT", async () => {
console.log("Shutting down the server...");

await db.close();

process.exit(0);
});
});
154 changes: 126 additions & 28 deletions server/models/Identity.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,131 @@
const Sequelize = require("sequelize");
const db = require("../utils/database");
const { encrypt, decrypt } = require("../utils/encryption");

module.exports = db.define("identities", {
name: {
type: Sequelize.STRING,
allowNull: false,
module.exports = db.define(
"identities",
{
name: {
type: Sequelize.STRING,
allowNull: false,
},
accountId: {
type: Sequelize.INTEGER,
allowNull: false,
},
username: {
type: Sequelize.STRING,
allowNull: true,
},
type: {
type: Sequelize.STRING,
allowNull: false,
},
password: {
type: Sequelize.STRING,
allowNull: true,
},
passwordIV: {
type: Sequelize.STRING,
allowNull: true,
},
passwordAuthTag: {
type: Sequelize.STRING,
allowNull: true,
},
sshKey: {
type: Sequelize.TEXT,
allowNull: true,
},
sshKeyIV: {
type: Sequelize.STRING,
allowNull: true,
},
sshKeyAuthTag: {
type: Sequelize.STRING,
allowNull: true,
},
passphrase: {
type: Sequelize.STRING,
allowNull: true,
},
passphraseIV: {
type: Sequelize.STRING,
allowNull: true,
},
passphraseAuthTag: {
type: Sequelize.STRING,
allowNull: true,
},
},
accountId: {
type: Sequelize.INTEGER,
allowNull: false,
},
username: {
type: Sequelize.STRING,
allowNull: true,
},
type: {
type: Sequelize.STRING,
allowNull: false,
},
password: {
type: Sequelize.STRING,
allowNull: true,
},
sshKey: {
type: Sequelize.STRING,
allowNull: true,
},
passphrase: {
type: Sequelize.STRING,
allowNull: true,
{
freezeTableName: true,
createdAt: false,
updatedAt: false,
hooks: {
beforeCreate: (identity) => {
if (identity.password) {
const encrypted = encrypt(identity.password);
identity.password = encrypted.encrypted;
identity.passwordIV = encrypted.iv;
identity.passwordAuthTag = encrypted.authTag;
}
if (identity.sshKey) {
const encrypted = encrypt(identity.sshKey);
identity.sshKey = encrypted.encrypted;
identity.sshKeyIV = encrypted.iv;
identity.sshKeyAuthTag = encrypted.authTag;
}
if (identity.passphrase) {
const encrypted = encrypt(identity.passphrase);
identity.passphrase = encrypted.encrypted;
identity.passphraseIV = encrypted.iv;
identity.passphraseAuthTag = encrypted.authTag;
}
},
beforeUpdate: (identity) => {
if (identity.password && identity.password !== "********") {
const encrypted = encrypt(identity.password);
identity.password = encrypted.encrypted;
identity.passwordIV = encrypted.iv;
identity.passwordAuthTag = encrypted.authTag;
}
if (identity.sshKey) {
const encrypted = encrypt(identity.sshKey);
identity.sshKey = encrypted.encrypted;
identity.sshKeyIV = encrypted.iv;
identity.sshKeyAuthTag = encrypted.authTag;
}
if (identity.passphrase && identity.passphrase !== "********") {
const encrypted = encrypt(identity.passphrase);
identity.passphrase = encrypted.encrypted;
identity.passphraseIV = encrypted.iv;
identity.passphraseAuthTag = encrypted.authTag;
}
},
afterFind: (identities) => {
const decryptField = (obj, field) => {
if (obj[field]) {
obj[field] = decrypt(
obj[field],
obj[`${field}IV`],
obj[`${field}AuthTag`]
);
}
};

if (Array.isArray(identities)) {
identities.forEach((identity) => {
decryptField(identity, "password");
decryptField(identity, "sshKey");
decryptField(identity, "passphrase");
});
} else if (identities) {
decryptField(identities, "password");
decryptField(identities, "sshKey");
decryptField(identities, "passphrase");
}
},
},
}
}, { freezeTableName: true, createdAt: false, updatedAt: false });
);
Loading