Skip to content

Commit

Permalink
feat: --force to renewCertificates.
Browse files Browse the repository at this point in the history
fix: fixed `renewCertificates` when renewing certificates created from using an old Bootstrap version.
fix: allowing `renewCertificates` without a preview --upgrade
  • Loading branch information
fboucquez committed Jan 20, 2022
1 parent 9b12ae5 commit 627db1a
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 40 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -4,14 +4,16 @@ All notable changes to this project will be documented in this file.

The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [1.1.3] - NEXT
## [1.1.3] - Jan-20-2022

**Milestone**: Mainnet(1.0.3.1)

| Package | Version | Link |
| ---------------- |---------| ------------------------------------------------------------------ |
| Symbol Bootstrap | v1.1.3 | [symbol-bootstrap](https://www.npmjs.com/package/symbol-bootstrap) |

- Added `--force` to `renewCertificates`.
- Fixed `renewCertificates` when renewing certificates created using an old Bootstrap version.

## [1.1.2] - Jan-17-2022

Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -249,7 +249,7 @@ If you don't like it, let me know by creating issues on GitHub. Pull Requests ar
* [`symbol-bootstrap link`](docs/link.md) - It announces VRF and Voting Link transactions to the network for each node with 'Peer' or 'Voting' roles. This command finalizes the node registration to an existing network.
* [`symbol-bootstrap modifyMultisig`](docs/modifyMultisig.md) - Create or modify a multisig account
* [`symbol-bootstrap pack`](docs/pack.md) - It configures and packages your node into a zip file that can be uploaded to the final node machine.
* [`symbol-bootstrap renewCertificates`](docs/renewCertificates.md) - It renews the SSL certificates of the node regenerating the main ca.cert.pem and node.csr.pem files but reusing the current private keys.
* [`symbol-bootstrap renewCertificates`](docs/renewCertificates.md) - It renews the SSL certificates of the node regenerating the node.csr.pem files but reusing the current private keys.
* [`symbol-bootstrap report`](docs/report.md) - it generates reStructuredText (.rst) reports describing the configuration of each node.
* [`symbol-bootstrap resetData`](docs/resetData.md) - It removes the data keeping the generated configuration, certificates, keys and block 1.
* [`symbol-bootstrap run`](docs/run.md) - It boots the network via docker using the generated `docker-compose.yml` file and configuration. The config and compose methods/commands need to be called before this method. This is just a wrapper for the `docker-compose up` bash call.
Expand Down
11 changes: 9 additions & 2 deletions docs/renewCertificates.md
@@ -1,7 +1,9 @@
`symbol-bootstrap renewCertificates`
====================================

It renews the SSL certificates of the node regenerating the main ca.cert.pem and node.csr.pem files but reusing the current private keys.
It renews the SSL certificates of the node regenerating the node.csr.pem files but reusing the current private keys.

The certificates are only regenerated when they are closed to expiration (30 days). If you want to renew anyway, use the --force param.

This command does not change the node private key (yet). This change would require a harvesters.dat migration and relinking the node key.

Expand All @@ -11,7 +13,7 @@ It's recommended to backup the target folder before running this operation!

## `symbol-bootstrap renewCertificates`

It renews the SSL certificates of the node regenerating the main ca.cert.pem and node.csr.pem files but reusing the current private keys.
It renews the SSL certificates of the node regenerating the node.csr.pem files but reusing the current private keys.

```
USAGE
Expand All @@ -30,6 +32,8 @@ OPTIONS
-u, --user=user [default: current] User used to run docker images when generating the certificates.
"current" means the current user.
--force Renew the certificates even though they are not close to expire.
--logger=logger [default: Console,File] The loggers the command will use. Options are:
Console,File,Silent. Use ',' to select multiple loggers.
Expand All @@ -42,6 +46,9 @@ OPTIONS
(--noPassword).
DESCRIPTION
The certificates are only regenerated when they are closed to expiration (30 days). If you want to renew anyway, use
the --force param.
This command does not change the node private key (yet). This change would require a harvesters.dat migration and
relinking the node key.
Expand Down
34 changes: 25 additions & 9 deletions src/commands/renewCertificates.ts
Expand Up @@ -16,11 +16,13 @@
import { Command, flags } from '@oclif/command';
import { Account } from 'symbol-sdk';
import { LoggerFactory, System } from '../logger';
import { CertificatePair, ConfigAccount, ConfigPreset } from '../model';
import { BootstrapUtils, CertificateService, CommandUtils, ConfigLoader } from '../service';
import { CertificatePair, ConfigAccount } from '../model';
import { BootstrapUtils, CertificateService, CommandUtils, ConfigLoader, RenewMode } from '../service';

export default class RenewCertificates extends Command {
static description = `It renews the SSL certificates of the node regenerating the main ca.cert.pem and node.csr.pem files but reusing the current private keys.
static description = `It renews the SSL certificates of the node regenerating the node.csr.pem files but reusing the current private keys.
The certificates are only regenerated when they are closed to expiration (30 days). If you want to renew anyway, use the --force param.
This command does not change the node private key (yet). This change would require a harvesters.dat migration and relinking the node key.
Expand All @@ -44,6 +46,11 @@ It's recommended to backup the target folder before running this operation!
description: `User used to run docker images when generating the certificates. "${BootstrapUtils.CURRENT_USER}" means the current user.`,
default: BootstrapUtils.CURRENT_USER,
}),

force: flags.boolean({
description: `Renew the certificates even though they are not close to expire.`,
default: false,
}),
logger: CommandUtils.getLoggerFlag(...System),
};

Expand All @@ -60,19 +67,23 @@ It's recommended to backup the target folder before running this operation!
);
const target = flags.target;
const configLoader = new ConfigLoader(logger);
const presetData = configLoader.loadExistingPresetData(target, password);
const addresses = configLoader.loadExistingAddresses(target, password);
const customPreset = configLoader.loadCustomPreset(flags.customPreset, password);
const mergedPresetData: ConfigPreset = configLoader.mergePresets(presetData, customPreset);

const oldPresetData = configLoader.loadExistingPresetData(target, password);
const presetData = configLoader.createPresetData({
workingDir: BootstrapUtils.defaultWorkingDir,
customPreset: flags.customPreset,
password: password,
oldPresetData,
});
const addresses = configLoader.loadExistingAddresses(target, password);
const networkType = presetData.networkType;
const certificateService = new CertificateService(logger, {
target,
user: flags.user,
});
const certificateUpgraded = (
await Promise.all(
(mergedPresetData.nodes || []).map((nodePreset, index) => {
(presetData.nodes || []).map((nodePreset, index) => {
const nodeAccount = addresses.nodes?.[index];
if (!nodeAccount) {
throw new Error(`There is not node in addresses at index ${index}`);
Expand All @@ -90,7 +101,12 @@ It's recommended to backup the target folder before running this operation!
main: resolveAccount(nodeAccount.main, nodePreset.mainPrivateKey),
transport: resolveAccount(nodeAccount.transport, nodePreset.transportPrivateKey),
};
return certificateService.run(mergedPresetData, nodePreset.name, providedCertificates, true);
return certificateService.run(
presetData,
nodePreset.name,
providedCertificates,
flags.force ? RenewMode.ALWAYS : RenewMode.WHEN_REQUIRED,
);
}),
)
).find((f) => f);
Expand Down
56 changes: 37 additions & 19 deletions src/service/CertificateService.ts
Expand Up @@ -49,6 +49,12 @@ export interface CertificateConfigPreset {
certificateExpirationWarningInDays: number;
}

export enum RenewMode {
ONLY_WARNING,
WHEN_REQUIRED,
ALWAYS,
}

export class CertificateService {
public static NODE_CERTIFICATE_FILE_NAME = 'node.crt.pem';
public static CA_CERTIFICATE_FILE_NAME = 'ca.cert.pem';
Expand All @@ -64,7 +70,7 @@ export class CertificateService {
presetData: CertificateConfigPreset,
name: string,
providedCertificates: NodeCertificates,
renewIfRequired: boolean,
renewMode: RenewMode,
customCertFolder?: string,
randomSerial?: string,
): Promise<boolean> {
Expand All @@ -77,26 +83,34 @@ export class CertificateService {
CertificateService.NODE_CERTIFICATE_FILE_NAME,
presetData.certificateExpirationWarningInDays,
);

if (willExpireReport.willExpire) {
if (renewIfRequired) {
this.logger.info(
`The ${CertificateService.NODE_CERTIFICATE_FILE_NAME} certificate for node ${name} will expire in less than ${presetData.certificateExpirationWarningInDays} days on ${willExpireReport.expirationDate}. Renewing...`,
);
await this.createCertificate(true, presetData, certFolder, name, providedCertificates, metadataFile, randomSerial);
return true;
const shouldRenew = (willExpireReport.willExpire && renewMode == RenewMode.WHEN_REQUIRED) || renewMode == RenewMode.ALWAYS;
const logWarning =
(willExpireReport.willExpire && renewMode == RenewMode.ONLY_WARNING) ||
(!willExpireReport.willExpire && renewMode == RenewMode.ALWAYS);

const resolveRenewMessage = (): string => {
if (willExpireReport.willExpire) {
if (renewMode == RenewMode.WHEN_REQUIRED || renewMode == RenewMode.ALWAYS) {
return `The ${CertificateService.NODE_CERTIFICATE_FILE_NAME} certificate for node ${name} will expire in less than ${presetData.certificateExpirationWarningInDays} days on ${willExpireReport.expirationDate}. Renewing...`;
} else {
return `The ${CertificateService.NODE_CERTIFICATE_FILE_NAME} certificate for node ${name} will expire in less than ${presetData.certificateExpirationWarningInDays} days on ${willExpireReport.expirationDate}. You need to renew it.`;
}
} else {
this.logger.warn(
`The ${CertificateService.NODE_CERTIFICATE_FILE_NAME} certificate for node ${name} will expire in less than ${presetData.certificateExpirationWarningInDays} days on ${willExpireReport.expirationDate}. You need to renew it.`,
);
return false;
if (renewMode == RenewMode.ALWAYS) {
return `The ${CertificateService.NODE_CERTIFICATE_FILE_NAME} certificate for node ${name} will expire on ${willExpireReport.expirationDate}, renewing anyway...`;
} else {
return `The ${CertificateService.NODE_CERTIFICATE_FILE_NAME} certificate for node ${name} will expire on ${willExpireReport.expirationDate}. No need to renew it yet.`;
}
}
} else {
this.logger.info(
`The ${CertificateService.NODE_CERTIFICATE_FILE_NAME} certificate for node ${name} will expire on ${willExpireReport.expirationDate}. No need to renew it yet.`,
);
return false;
};
const message = resolveRenewMessage();
if (logWarning) this.logger.warn(message);
else this.logger.info(message);

if (shouldRenew) {
await this.createCertificate(true, presetData, certFolder, name, providedCertificates, metadataFile, randomSerial);
}
return shouldRenew;
} else {
await this.createCertificate(false, presetData, certFolder, name, providedCertificates, metadataFile, randomSerial);
return true;
Expand Down Expand Up @@ -170,7 +184,7 @@ export class CertificateService {
if (certificates.length != 2) {
throw new Error('Certificate creation failed. 2 certificates should have been created but got: ' + certificates.length);
}
this.logger.info(renew ? `Certificate for node ${name} renewed` : `Certificate for node ${name} created`);
this.logger.info(renew ? `Certificate for node ${name} renewed.` : `Certificate for node ${name} created.`);
const caCertificate = certificates[0];
const nodeCertificate = certificates[1];

Expand Down Expand Up @@ -213,6 +227,10 @@ export class CertificateService {
`;
return `set -e
# Clean up old versions files.
rm -rf new_certs
rm -f index.txt*
mkdir new_certs
chmod 700 new_certs
touch index.txt.attr
Expand Down
9 changes: 7 additions & 2 deletions src/service/ConfigService.ts
Expand Up @@ -34,7 +34,7 @@ import {
import { Logger } from '../logger';
import { Addresses, ConfigPreset, CustomPreset, GatewayConfigPreset, NodeAccount, PeerInfo } from '../model';
import { BootstrapUtils, KnownError, Password } from './BootstrapUtils';
import { CertificateService } from './CertificateService';
import { CertificateService, RenewMode } from './CertificateService';
import { CommandUtils } from './CommandUtils';
import { ConfigLoader } from './ConfigLoader';
import { CryptoUtils } from './CryptoUtils';
Expand Down Expand Up @@ -323,7 +323,12 @@ export class ConfigService {
main: account.main,
transport: account.transport,
};
return new CertificateService(this.logger, this.params).run(presetData, account.name, providedCertificates, false);
return new CertificateService(this.logger, this.params).run(
presetData,
account.name,
providedCertificates,
RenewMode.ONLY_WARNING,
);
}),
);
}
Expand Down
51 changes: 45 additions & 6 deletions test/service/CertificateService.test.ts
Expand Up @@ -28,6 +28,7 @@ import {
ConfigLoader,
NodeCertificates,
Preset,
RenewMode,
RuntimeService,
} from '../../src/service';

Expand Down Expand Up @@ -87,7 +88,7 @@ describe('CertificateService', () => {
BootstrapUtils.deleteFolder(logger, target);

const service = new CertificateService(logger, { target: target, user: await runtimeService.getDockerUserGroup() });
await service.run(presetData, name, keys, false, target, randomSerial);
await service.run(presetData, name, keys, RenewMode.ONLY_WARNING, target, randomSerial);

const expectedMetadata: CertificateMetadata = {
version: 1,
Expand All @@ -104,7 +105,7 @@ describe('CertificateService', () => {
const caCertificateExpirationInDays = presetData.caCertificateExpirationInDays;

const service = new CertificateService(logger, { target: target, user: await runtimeService.getDockerUserGroup() });
await service.run(presetData, name, keys, false, target, randomSerial);
await service.run(presetData, name, keys, RenewMode.ONLY_WARNING, target, randomSerial);

async function willExpire(certificateFileName: string, certificateExpirationWarningInDays: number): Promise<boolean> {
const report = await service.willCertificateExpire(
Expand Down Expand Up @@ -133,25 +134,63 @@ describe('CertificateService', () => {
}

// First time generation
expect(await service.run({ ...presetData, nodeCertificateExpirationInDays: 10 }, name, keys, true, target)).eq(true);
expect(await service.run({ ...presetData, nodeCertificateExpirationInDays: 10 }, name, keys, RenewMode.WHEN_REQUIRED, target)).eq(
true,
);
await verifyCertFolder();
const originalCaFile = await getCertFile(CertificateService.CA_CERTIFICATE_FILE_NAME);
const originalNodeFile = await getCertFile(CertificateService.NODE_CERTIFICATE_FILE_NAME);

//Renew is not required
expect(await service.run({ ...presetData, certificateExpirationWarningInDays: 9 }, name, keys, true, target)).eq(false);
expect(await service.run({ ...presetData, certificateExpirationWarningInDays: 9 }, name, keys, RenewMode.WHEN_REQUIRED, target)).eq(
false,
);
await verifyCertFolder();
expect(await getCertFile(CertificateService.CA_CERTIFICATE_FILE_NAME)).eq(originalCaFile);
expect(await getCertFile(CertificateService.NODE_CERTIFICATE_FILE_NAME)).eq(originalNodeFile);

//Renew is required but not auto-upgrade
expect(await service.run({ ...presetData, certificateExpirationWarningInDays: 11 }, name, keys, false, target)).eq(false);
expect(await service.run({ ...presetData, certificateExpirationWarningInDays: 11 }, name, keys, RenewMode.ONLY_WARNING, target)).eq(
false,
);
await verifyCertFolder();
expect(await getCertFile(CertificateService.CA_CERTIFICATE_FILE_NAME)).eq(originalCaFile);
expect(await getCertFile(CertificateService.NODE_CERTIFICATE_FILE_NAME)).eq(originalNodeFile);

//Renew is required and auto-upgrade
expect(await service.run({ ...presetData, certificateExpirationWarningInDays: 11 }, name, keys, true, target)).eq(true);
expect(
await service.run({ ...presetData, certificateExpirationWarningInDays: 11 }, name, keys, RenewMode.WHEN_REQUIRED, target),
).eq(true);
await verifyCertFolder();
expect(await getCertFile(CertificateService.CA_CERTIFICATE_FILE_NAME)).eq(originalCaFile);
expect(await getCertFile(CertificateService.NODE_CERTIFICATE_FILE_NAME)).not.eq(originalNodeFile); // Renewed
});

it('create and renew certificates always', async () => {
const target = 'target/tests/CertificateService.test';
BootstrapUtils.deleteFolder(logger, target);
const service = new CertificateService(logger, { target: target, user: await runtimeService.getDockerUserGroup() });

async function getCertFile(certificateFileName: string): Promise<string> {
return readFileSync(join(target, certificateFileName), 'utf-8');
}

// First time generation
expect(await service.run({ ...presetData }, name, keys, RenewMode.ONLY_WARNING, target)).eq(true);
await verifyCertFolder();
const originalCaFile = await getCertFile(CertificateService.CA_CERTIFICATE_FILE_NAME);
const originalNodeFile = await getCertFile(CertificateService.NODE_CERTIFICATE_FILE_NAME);

//Renew is not required
expect(
await service.run({ ...presetData, certificateExpirationWarningInDays: 50 }, name, keys, RenewMode.WHEN_REQUIRED, target),
).eq(false);
await verifyCertFolder();
expect(await getCertFile(CertificateService.CA_CERTIFICATE_FILE_NAME)).eq(originalCaFile);
expect(await getCertFile(CertificateService.NODE_CERTIFICATE_FILE_NAME)).eq(originalNodeFile);

//Renew is not required but always
expect(await service.run({ ...presetData, certificateExpirationWarningInDays: 50 }, name, keys, RenewMode.ALWAYS, target)).eq(true);
await verifyCertFolder();
expect(await getCertFile(CertificateService.CA_CERTIFICATE_FILE_NAME)).eq(originalCaFile);
expect(await getCertFile(CertificateService.NODE_CERTIFICATE_FILE_NAME)).not.eq(originalNodeFile); // Renewed
Expand Down

0 comments on commit 627db1a

Please sign in to comment.