Skip to content

Commit

Permalink
Add license check for FIPS (#181187)
Browse files Browse the repository at this point in the history
## Updates
### Latest updates

- Expose whether KB is configured to run in FIPS mode from Core ->
Security
<img width="653" alt="Screenshot 2024-06-20 at 9 55 17 PM"
src="https://github.com/elastic/kibana/assets/21210601/56a9f50f-0a05-41ca-9292-ed225b3d8062">



Consolidating all FIPS PRs into this PR

*Previous PRs were Approved

### Changes

- Config option is now experimental:
`xpack.security.experimental.fipsMode.enabled`
- Documentation has been revised
  - Listed as an experimental feature
  - Added keystore references for adding a password

## Summary
Closes #169738
Closes #169739
Closes #169740
Closes #185948

FIPS is a platinum license feature. 

KIbana instances must have a platinum or better license to start up in
FIPS mode, a lesser license will result in Kibana failing to start up

If the license is degraded, Kibana will still run, but an error will be
logged letting the user know that Kibana will not be able to restart.

## Config changes

This PR required the changes that were approved from [a previous
PR](#174558), since that PR
couldn't be merged into main, I merged it here.

## Testing

### Locally

In your `kibana.dev.yml` add:
`xpack.security.experimental.fipsMode.enabled: true`

To allow Kibana to start without actually providing a compliant OpenSSL
provider, in `x-pack/plugins/security/server/config.ts` change L328 from
`if (isFipsEnabled !== isNodeRunningWithFipsEnabled)` to `if (false)`

You are now configured to run in FIPS-spoof mode!

Run: `yarn es snapshot` and `yarn start` > You should see Kibana fail to
start with an error about using a basic license.

Run: `yarn es snapshot --license trial` and `yarn start` > Kibana should
start.

Login as `elastic` and navigate to Stack Management > License Management

Switch your license to `basic` and accept.

In your logs, you will see an error letting users know that you no
longer have an appropriate license and Kibana will not restart.


### For FIPS enthusiasts

Start an ES instance in a method of your choosing, but not using `yarn
es snapshot`. I like to use an 8.15.0-snapshot from the `.es/cache`
directory by running `tar -xzvf
elasticsearch-8.15.0-SNAPSHOT-darwin-aarch64.tar.gz ` and cd into the
new directory's `bin` folder to run `./elasticsearch`

Ensure you have Docker running locally.

From any command line, run: `docker run --rm -it -e
XPACK_SECURITY_FIPSMODE_ENABLED='true' -p 5601:5601/tcp
docker.elastic.co/kibana-ci/kibana-ubi-fips:8.15.0-SNAPSHOT-bc3150316ed317c08d57c6bd785ba39586072e1d`

This will start Kibana into Interactive Setup mode, copy and paste the
token from the ES startup logs.

Kibana should fail to start and you should see Kibana fail to start with
an error about using a basic license.

Repeat the above process except before you paste the token from ES, do
the following to enable a trial license on your ES instance:

In a new terminal window, navigate to your the top level of your
elasticsearch folder and run

`curl -X POST --cacert config/certs/http_ca.crt -u
elastic:YOUR_PASSWORD_HERE
"https://localhost:9200/_license/start_trial?acknowledge=true&pretty"`

You should receive a successful response.

Now paste the token from the ES startup logs into the Kibana Interactive
Setup window and Kibana should start.

Login as `elastic` and navigate to Stack Management > License Management

Switch your license to `basic` and accept.

In your logs, you will see an error letting users know that you no
longer have an appropriate license and Kibana will not restart.

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: lcawl <[email protected]>
  • Loading branch information
3 people authored Jul 2, 2024
1 parent ae30c30 commit 4554b75
Show file tree
Hide file tree
Showing 31 changed files with 679 additions and 5 deletions.
63 changes: 63 additions & 0 deletions docs/user/security/fips-140-2.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[[xpack-security-fips-140-2]]
=== FIPS 140-2

experimental::[]

The Federal Information Processing Standard (FIPS) Publication 140-2, (FIPS PUB 140-2),
titled "Security Requirements for Cryptographic Modules" is a U.S. government computer security standard
used to approve cryptographic modules.

{kib} offers a FIPS 140-2 compliant mode and as such can run in a Node.js environment configured with a FIPS
140-2 compliant OpenSSL3 provider.

To run {kib} in FIPS mode, you must have the appropriate {subscriptions}[subscription].

[IMPORTANT]
============================================================================
The Node bundled with {kib} is not configured for FIPS 140-2. You must configure a FIPS 140-2 compliant OpenSSL3
provider. Consult the Node.js documentation to learn how to configure your environment.
============================================================================

For {kib}, adherence to FIPS 140-2 is ensured by:

* Using FIPS approved / NIST recommended cryptographic algorithms.

* Delegating the implementation of these cryptographic algorithms to a NIST validated cryptographic module
(available via Node.js configured with an OpenSSL3 provider).

* Allowing the configuration of {kib} in a FIPS 140-2 compliant manner, as documented below.

==== Configuring {kib} for FIPS 140-2

Apart from setting `xpack.security.experimental.fipsMode.enabled` to `true` in your {kib} config, a number of security related
settings need to be reviewed and configured in order to run {kib} successfully in a FIPS 140-2 compliant Node.js
environment.

===== Kibana keystore

FIPS 140-2 (via NIST Special Publication 800-132) dictates that encryption keys should at least have an effective
strength of 112 bits. As such, the Kibana keystore that stores the application’s secure settings needs to be
password protected with a password that satisfies this requirement. This means that the password needs to be 14 bytes
long which is equivalent to a 14 character ASCII encoded password, or a 7 character UTF-8 encoded password.

For more information on how to set this password, refer to the <<change-password,keystore documentation>>.

===== TLS keystore and keys

Keystores can be used in a number of General TLS settings in order to conveniently store key and trust material.
PKCS#12 keystores cannot be used in a FIPS 140-2 compliant Node.js environment. Avoid using these types of keystores.
Your FIPS 140-2 provider may provide a compliant keystore implementation that can be used, or you can use PEM encoded
files. To use PEM encoded key material, you can use the relevant `\*.key` and `*.certificate` configuration options,
and for trust material you can use `*.certificate_authorities`.

As an example, avoid PKCS#12 specific settings such as:

* `server.ssl.keystore.path`
* `server.ssl.truststore.path`
* `elasticsearch.ssl.keystore.path`
* `elasticsearch.ssl.truststore.path`

===== Limitations

Configuring {kib} to run in FIPS mode is still considered to be experimental. Not all features are guaranteed to
function as expected.
1 change: 1 addition & 0 deletions docs/user/security/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ include::authorization/index.asciidoc[]
include::authorization/kibana-privileges.asciidoc[]
include::api-keys/index.asciidoc[]
include::role-mappings/index.asciidoc[]
include::fips-140-2.asciidoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>({
},
security: {
registerSecurityDelegate: (api) => deps.security.registerSecurityDelegate(api),
fips: deps.security.fips,
},
userProfile: {
registerUserProfileDelegate: (delegate) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

const mockGetFipsFn = jest.fn();
jest.mock('crypto', () => ({
randomBytes: jest.fn(),
constants: jest.requireActual('crypto').constants,
get getFips() {
return mockGetFipsFn;
},
}));

import { SecurityServiceConfigType } from '../utils';
import { isFipsEnabled, checkFipsConfig } from './fips';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';

describe('fips', () => {
let config: SecurityServiceConfigType;
describe('#isFipsEnabled', () => {
it('should return `true` if config.experimental.fipsMode.enabled is `true`', () => {
config = { experimental: { fipsMode: { enabled: true } } };

expect(isFipsEnabled(config)).toBe(true);
});

it('should return `false` if config.experimental.fipsMode.enabled is `false`', () => {
config = { experimental: { fipsMode: { enabled: false } } };

expect(isFipsEnabled(config)).toBe(false);
});

it('should return `false` if config.experimental.fipsMode.enabled is `undefined`', () => {
expect(isFipsEnabled(config)).toBe(false);
});
});

describe('checkFipsConfig', () => {
let mockExit: jest.SpyInstance;

beforeAll(() => {
mockExit = jest.spyOn(process, 'exit').mockImplementation((exitCode) => {
throw new Error(`Fake Exit: ${exitCode}`);
});
});

afterAll(() => {
mockExit.mockRestore();
});

it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode false', async () => {
config = { experimental: { fipsMode: { enabled: true } } };
const logger = loggingSystemMock.create().get();
try {
checkFipsConfig(config, logger);
} catch (e) {
expect(mockExit).toHaveBeenNthCalledWith(1, 78);
}

expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
"Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to true and the configured Node.js environment has FIPS disabled",
],
]
`);
});

it('should log an error message if FIPS mode is misconfigured - xpack.security.experimental.fipsMode.enabled false, Nodejs FIPS mode true', async () => {
mockGetFipsFn.mockImplementationOnce(() => {
return 1;
});

config = { experimental: { fipsMode: { enabled: false } } };
const logger = loggingSystemMock.create().get();

try {
checkFipsConfig(config, logger);
} catch (e) {
expect(mockExit).toHaveBeenNthCalledWith(1, 78);
}

expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
"Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to false and the configured Node.js environment has FIPS enabled",
],
]
`);
});

it('should log an info message if FIPS mode is properly configured - xpack.security.experimental.fipsMode.enabled true, Nodejs FIPS mode true', async () => {
mockGetFipsFn.mockImplementationOnce(() => {
return 1;
});

config = { experimental: { fipsMode: { enabled: true } } };
const logger = loggingSystemMock.create().get();

try {
checkFipsConfig(config, logger);
} catch (e) {
logger.error('Should not throw error!');
}

expect(loggingSystemMock.collect(logger).info).toMatchInlineSnapshot(`
Array [
Array [
"Kibana is running in FIPS mode.",
],
]
`);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { Logger } from '@kbn/logging';
import { getFips } from 'crypto';
import { SecurityServiceConfigType } from '../utils';

export function isFipsEnabled(config: SecurityServiceConfigType): boolean {
return config?.experimental?.fipsMode?.enabled ?? false;
}

export function checkFipsConfig(config: SecurityServiceConfigType, logger: Logger) {
const isFipsConfigEnabled = isFipsEnabled(config);
const isNodeRunningWithFipsEnabled = getFips() === 1;

// Check if FIPS is enabled in either setting
if (isFipsConfigEnabled || isNodeRunningWithFipsEnabled) {
// FIPS must be enabled on both or log and error an exit Kibana
if (isFipsConfigEnabled !== isNodeRunningWithFipsEnabled) {
logger.error(
`Configuration mismatch error. xpack.security.experimental.fipsMode.enabled is set to ${isFipsConfigEnabled} and the configured Node.js environment has FIPS ${
isNodeRunningWithFipsEnabled ? 'enabled' : 'disabled'
}`
);
process.exit(78);
} else {
logger.info('Kibana is running in FIPS mode.');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ describe('SecurityService', () => {
);
});
});

describe('#fips', () => {
describe('#isEnabled', () => {
it('should return boolean', () => {
const { fips } = service.setup();

expect(fips.isEnabled()).toBe(false);
});
});
});
});

describe('#start', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,59 @@
import type { Logger } from '@kbn/logging';
import type { CoreContext, CoreService } from '@kbn/core-base-server-internal';
import type { CoreSecurityDelegateContract } from '@kbn/core-security-server';
import { Observable, Subscription } from 'rxjs';
import { Config } from '@kbn/config';
import { isFipsEnabled, checkFipsConfig } from './fips/fips';
import type {
InternalSecurityServiceSetup,
InternalSecurityServiceStart,
} from './internal_contracts';
import { getDefaultSecurityImplementation, convertSecurityApi } from './utils';
import {
getDefaultSecurityImplementation,
convertSecurityApi,
SecurityServiceConfigType,
} from './utils';

export class SecurityService
implements CoreService<InternalSecurityServiceSetup, InternalSecurityServiceStart>
{
private readonly log: Logger;
private securityApi?: CoreSecurityDelegateContract;
private config$: Observable<Config>;
private configSubscription?: Subscription;
private config: Config | undefined;
private readonly getConfig = () => {
if (!this.config) {
throw new Error('Config is not available.');
}
return this.config;
};

constructor(coreContext: CoreContext) {
this.log = coreContext.logger.get('security-service');

this.config$ = coreContext.configService.getConfig$();
this.configSubscription = this.config$.subscribe((config) => {
this.config = config;
});
}

public setup(): InternalSecurityServiceSetup {
const config = this.getConfig();
const securityConfig: SecurityServiceConfigType = config.get(['xpack', 'security']);

checkFipsConfig(securityConfig, this.log);

return {
registerSecurityDelegate: (api) => {
if (this.securityApi) {
throw new Error('security API can only be registered once');
}
this.securityApi = api;
},
fips: {
isEnabled: () => isFipsEnabled(securityConfig),
},
};
}

Expand All @@ -44,5 +73,10 @@ export class SecurityService
return convertSecurityApi(apiContract);
}

public stop() {}
public stop() {
if (this.configSubscription) {
this.configSubscription.unsubscribe();
this.configSubscription = undefined;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@

export { convertSecurityApi } from './convert_security_api';
export { getDefaultSecurityImplementation } from './default_implementation';

export interface SecurityServiceConfigType {
experimental?: {
fipsMode?: {
enabled: boolean;
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@
"@kbn/core-http-server",
"@kbn/logging-mocks",
"@kbn/core-base-server-mocks",
"@kbn/config",
"@kbn/core-logging-server-mocks",
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { auditServiceMock, type MockedAuditService } from './audit.mock';
const createSetupMock = () => {
const mock: jest.Mocked<SecurityServiceSetup> = {
registerSecurityDelegate: jest.fn(),
fips: { isEnabled: jest.fn() },
};

return mock;
Expand All @@ -43,6 +44,7 @@ const createStartMock = (): SecurityStartMock => {
const createInternalSetupMock = () => {
const mock: jest.Mocked<InternalSecurityServiceSetup> = {
registerSecurityDelegate: jest.fn(),
fips: { isEnabled: jest.fn() },
};

return mock;
Expand Down
1 change: 1 addition & 0 deletions packages/core/security/core-security-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export type {
AuditRequest,
} from './src/audit_logging/audit_events';
export type { AuditLogger } from './src/audit_logging/audit_logger';
export type { CoreFipsService } from './src/fips';
6 changes: 6 additions & 0 deletions packages/core/security/core-security-server/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Side Public License, v 1.
*/

import type { CoreFipsService } from './fips';
import type { CoreAuthenticationService } from './authc';
import type { CoreSecurityDelegateContract } from './api_provider';
import type { CoreAuditService } from './audit';
Expand All @@ -21,6 +22,11 @@ export interface SecurityServiceSetup {
* @remark this should **exclusively** be used by the security plugin.
*/
registerSecurityDelegate(api: CoreSecurityDelegateContract): void;

/**
* The {@link CoreFipsService | FIPS service}
*/
fips: CoreFipsService;
}

/**
Expand Down
Loading

0 comments on commit 4554b75

Please sign in to comment.