Skip to content
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
16 changes: 16 additions & 0 deletions .bin/scripts/ovh/ovh-nodejs-client/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:node/recommended",
"plugin:prettier/recommended"
],
"env": {
"node": true,
"es6": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2018
}
}
53 changes: 53 additions & 0 deletions .bin/scripts/ovh/ovh-nodejs-client/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import env from "env-var";
import open from "open";
import ovh from "@ovhcloud/node-ovh";

function newOvhClient(consumerKey) {
const appKey = env.get("OVH_API_APP_KEY").asString();
const appSecret = env.get("OVH_API_APP_SECRET").asString();

if (!appKey || !appSecret || appKey === 'null' || appSecret === 'null') {
throw new Error("OVH_API_APP_KEY and OVH_API_APP_SECRET environment variables must be set.");
}

let client = ovh({
endpoint: "ovh-eu",
appKey,
appSecret,
...(consumerKey ? { consumerKey } : {}),
});
return client;
}

export async function getClient(key) {
if (key) {
return newOvhClient(key);
}

let client = newOvhClient();
let { consumerKey, validationUrl, ...rest } = await client.requestPromised("POST", "/auth/credential", {
accessRules: [
{ method: "POST", path: "/auth/*" },
{ method: "GET", path: "/ip" },
{ method: "GET", path: "/ip/*" },
{ method: "PUT", path: "/ip/*" },
{ method: "POST", path: "/ip/*" },
{ method: "DELETE", path: "/ip/*" },
{ method: "GET", path: "/dedicated/*" },
{ method: "POST", path: "/dedicated/*" },
{ method: "GET", path: "/vps" },
{ method: "GET", path: "/vps/*" },
{ method: "GET", path: "/cloud" },
{ method: "GET", path: "/cloud/*" },
],
});

console.log(`Waiting 20 sec for the key ${consumerKey} to be validated...`);
await open(validationUrl);

return new Promise((resolve) => {
setTimeout(() => {
resolve(newOvhClient(consumerKey));
}, 20000);
});
}
242 changes: 242 additions & 0 deletions .bin/scripts/ovh/ovh-nodejs-client/firewall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import getConfig from "./getConfig.js";
import { resourceExists, resourceOrNull } from "./utils.js";

const allowTcpConnection = (sequence) => {
return {
sequence,
action: "permit",
protocol: "tcp",
source: null,
sourcePort: null,
destinationPort: null,
tcpOption: { option: "established" },
};
};

const allowTcpOnPort = (sequence, port, source = null) => {
return {
sequence,
action: "permit",
protocol: "tcp",
destinationPort: port,
source,
sourcePort: null,
tcpOption: {},
};
};
const allowUdpOnPort = (sequence, port) => {
return {
sequence,
action: "permit",
protocol: "udp",
destinationPort: port,
source: null,
sourcePort: null,
};
};
const allowICMP = (sequence) => {
return {
sequence,
protocol: "icmp",
action: "permit",
source: null,
sourcePort: null,
destinationPort: null,
};
};
const denyAllTcp = (sequence) => {
return {
sequence,
action: "deny",
protocol: "tcp",
source: null,
sourcePort: null,
destinationPort: null,
tcpOption: {},
};
};
const denyAllUdp = (sequence) => {
return {
sequence,
action: "deny",
protocol: "udp",
source: null,
sourcePort: null,
destinationPort: null,
};
};

// Rule definition doesn't have same shape as the rule object
const ruleNeedsUpdate = (def, currentRule = null) => {
const defTcpOption = def.tcpOption?.option ?? null;
const defDestPort = def.destinationPort ? `eq ${def.destinationPort}` : def.destinationPort;
return (
currentRule === null ||
def.sequence !== currentRule.sequence ||
def.action !== currentRule.action ||
def.protocol !== currentRule.protocol ||
!["ok", "creationPending"].includes(currentRule.state) ||
def.sourcePort !== currentRule.sourcePort ||
defDestPort !== currentRule.destinationPort ||
currentRule.fragments ||
defTcpOption !== currentRule.tcpOption
);
};

function asIpBlock(ip) {
const mask = ip.includes(":") ? 128 : 32;
return encodeURIComponent(`${ip}/${mask}`);
}

async function firewallExists(client, ip) {
return resourceExists(client, () => {
let ipBlock = asIpBlock(ip);
return client.requestPromised("GET", `/ip/${ipBlock}/firewall/${ip}`);
});
}

async function getRule(client, ip, sequence) {
return resourceOrNull(client, () => {
let ipBlock = asIpBlock(ip);
return client.requestPromised("GET", `/ip/${ipBlock}/firewall/${ip}/rule/${sequence}`);
});
}

async function mitigationActivated(client, ip) {
return resourceExists(client, () => {
let ipBlock = asIpBlock(ip);
return client.requestPromised("GET", `/ip/${ipBlock}/mitigation/${ip}`);
});
}

async function createFirewall(client, ip) {
let ipBlock = asIpBlock(ip);

if (await firewallExists(client, ip)) {
console.log(`Firewall already created for ip '${ip}'`);
return;
}
console.log(`Creating firewall for ip '${ip}'...`);
return client.requestPromised("POST", `/ip/${ipBlock}/firewall`, { ipOnFirewall: ip });
}

async function updateRule(client, ip, def) {
let ipBlock = asIpBlock(ip);

const currentRule = await getRule(client, ip, def.sequence);
if (currentRule) {
if (!ruleNeedsUpdate(def, currentRule)) {
return;
}

console.log("Removing rule", currentRule);
await client.requestPromised("DELETE", `/ip/${ipBlock}/firewall/${ip}/rule/${currentRule.sequence}`);
console.log("Waiting 60s for rule to be removed");
await new Promise((resolve) => setTimeout(resolve, 60_000));
}

console.log(`Creating rule`, def);
await client.requestPromised("POST", `/ip/${ipBlock}/firewall/${ip}/rule`, def);
}

async function updateRules(client, ip, defs) {
let ipBlock = asIpBlock(ip);

const existingSequence = await client.requestPromised("GET", `/ip/${ipBlock}/firewall/${ip}/rule`);
const expectedSequence = new Set(defs.map((def) => def.sequence));

const tasks = [];
for (let i = 0; i < defs.length; i++) {
tasks.push(updateRule(client, ip, defs[i]));
}
tasks.push(
...existingSequence
.filter((s) => !expectedSequence.has(Number(s)))
.map(async (s) => {
console.log("Removing rule", await getRule(client, ip, s));
return client.requestPromised("DELETE", `/ip/${ipBlock}/firewall/${ip}/rule/${s}`);
})
);

await Promise.all(tasks);
}

async function updateIpDescription(client, ip, product, env) {
const resp = await client.requestPromised("GET", `/ip/${asIpBlock(ip)}`);
const description = `${product}-${env}`;
if (resp.description !== description) {
await client.requestPromised("PUT", `/ip/${asIpBlock(ip)}`, { description: description });
}
}

async function configureFirewall(client, ip, product, env) {
await updateIpDescription(client, ip, product, env);

await createFirewall(client, ip);
const rules = [
allowTcpConnection(0),
allowTcpOnPort(1, 22),
allowTcpOnPort(2, 443),
allowTcpOnPort(3, 80),
allowICMP(10),
// denyAllUdp(18),
denyAllTcp(19),
];

if (product === "monitoring") {
rules.push(allowTcpOnPort(4, 444));
}

if (product === "orion") {
const envType = env.split("_")[0];
if (envType === "recette" || envType === "qualification") {
rules.push(allowTcpOnPort(4, 5432));
}
}

await updateRules(client, ip, rules);
console.log(`Firewall for ${ip} configured`);
}

async function activateMitigation(client, ip) {
let ipBlock = asIpBlock(ip);
if (await mitigationActivated(client, ip)) {
console.log(`Mitigation already activated for ip '${ip}'`);
return;
}

console.log(`Activating permanent mitigation...`);
await client.requestPromised("POST", `/ip/${ipBlock}/mitigation`, { ipOnMitigation: ip });
}

async function closeService(client, ip) {
if (await firewallExists(client, ip)) {
await updateRules(client, ip, [allowTcpConnection(0), allowTcpOnPort(1, 22), allowICMP(10), denyAllTcp(19)]);
} else {
console.log("Firewall does not exist, can't close service on port 443/80 !");
}
}

async function getAllIp(client, ip) {
const ipData = await client.requestPromised("GET", `/ip/${ip}`);

const cloudProjects = await client.requestPromised("GET", `/cloud/project`);

if (cloudProjects.includes(ipData.routedTo.serviceName)) {
const instances = await client.requestPromised("GET", `/cloud/project/${ipData.routedTo.serviceName}/instance`);
const instance = instances.find((instance) => instance.ipAddresses.some((i) => i.ip === ip));

if (!instance) {
throw new Error(`Instance not found for ip ${ip}`);
}

return instance.ipAddresses.filter((i) => i.version === 4 && i.type === "public").map((i) => i.ip);
}

const ips = await client.requestPromised("GET", `/vps/${ipData.routedTo.serviceName}/ips`);

// Returns all ipv4
return ips.filter((i) => i.includes(".") && !i.startsWith("10."));
}

export { configureFirewall, activateMitigation, closeService, getAllIp };
31 changes: 31 additions & 0 deletions .bin/scripts/ovh/ovh-nodejs-client/getConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { readdir, readFile } from 'fs/promises';
import { __dirname } from './utils.js';
import { join } from 'path';
import { parse } from 'ini';

// Récupération des associations de produits et de leurs IPs
export default async function getConfig() {
const productDir = join(__dirname(import.meta.url), '..', '..', '..', '..', 'products');
const files = await readdir(productDir, { withFileTypes: true });

const config = {};
for (const file of files) {
if (file.isDirectory()) {
const data = await readFile(join(productDir, file.name, 'env.ini'), 'utf-8');
const env = parse(data);
for (const key of Object.keys(env)) {
if (key.endsWith(':vars')) {
continue;
}

const ips = Object.keys(env[key]).filter(v => /^\d+\.\d+\.\d+\.\d+$/.test(v));
if (ips.length === 0) {
continue;
}
config[`${file.name}-${key}`] = ips
}
}
}

return config;
}
53 changes: 53 additions & 0 deletions .bin/scripts/ovh/ovh-nodejs-client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { program as cli } from "commander";
import { getClient } from "./api.js";
import { configureFirewall, activateMitigation, closeService, getAllIp } from "./firewall.js";

function handleError(e) {
console.error(e.constructor.name === "EnvVarError" ? e.message : e);
process.exit(1); // eslint-disable-line no-process-exit
}
process.on("unhandledRejection", handleError);
process.on("uncaughtException", handleError);

cli
.command("ping")
.description("Permet de verifier que la clé est valide")
.option("--key <key>", "La consumer key")
.action(async ({ key }) => {
let client = await getClient(key);

await client.requestPromised("GET", `/auth/time`);
});

cli
.command("createFirewall <ip> <product> <env>")
.description("Permet de créer/configurer le firewall et d'activer la mitigation")
.option("--key <key>", "La consumer key")
.action(async (ip, product, env, { key }) => {
let client = await getClient(key);

const ips = await getAllIp(client, ip);

for (const ipV4 of ips) {
await configureFirewall(client, ipV4, product, env);
await activateMitigation(client, ipV4);
console.log(`Firewall and mitigation activated for VPS ${ipV4}`);
}
});

cli
.command("closeService <ip> <product>")
.description("Permet de créer/configurer le firewall pour fermer le service sur les ports 80 et 443")
.option("--key <key>", "La consumer key")
.action(async (ip, { key }) => {
let client = await getClient(key);

const ips = await getAllIp(client, ip);

for (const ipV4 of ips) {
await closeService(client, ipV4);
console.log(`Service closed on port for VPS ${ipV4}.`);
}
});

cli.parse(process.argv);
Loading
Loading