Skip to content

Commit

Permalink
[actions] adds proxyBypassHosts and proxyOnlyHosts Kibana config keys
Browse files Browse the repository at this point in the history
resolves elastic#92949

This PR adds two new Kibana config keys to further customize when the proxy
is used when making HTTP requests.  Prior to this PR, if a proxy was set
via the `xpack.actions.proxyUrl` config key, all requests would be
proxied.

Now, there's a further refinement in that hostnames can be added
to the `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyBypassHosts`
config keys.  Only one of these config keys can be used at a time.

If the target URL hostname of the HTTP request is listed in the
`proxyBypassHosts` list, the proxy won't be used.

If the target URL hostname of the HTTP request is **NOT** listed in the
`proxyOnlyHosts` list, the proxy won't be used.

Depending on the customer's environment, it may be easier to list the hosts to
bypass, or easier to list the hosts that should only be proxied, so they can
choose either method.
  • Loading branch information
pmuellr committed Mar 29, 2021
1 parent 5d59c2c commit 790736e
Show file tree
Hide file tree
Showing 12 changed files with 97 additions and 32 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ describe('create()', () => {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});

const localActionTypeRegistryParams = {
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/server/actions_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const defaultActionsConfig: ActionsConfig = {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
};

describe('ensureUriAllowed', () => {
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/actions/server/actions_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,18 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet

return {
proxyUrl: config.proxyUrl,
proxyBypassHosts: arrayAsSet(config.proxyBypassHosts),
proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts),
proxyHeaders: config.proxyHeaders,
proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates,
};
}

function arrayAsSet<T>(arr: T[] | undefined): Set<T> | undefined {
if (!arr) return;
return new Set(arr);
}

export function getActionsConfigurationUtilities(
config: ActionsConfig
): ActionsConfigurationUtilities {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../../actions_config.mock';
import { getCustomAgents } from './get_custom_agents';

const TestUrl = 'https://elastic.co/foo/bar/baz';

const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
const configurationUtilities = actionsConfigMock.create();
jest.mock('axios');
Expand Down Expand Up @@ -66,17 +68,19 @@ describe('request', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyRejectUnauthorizedCertificates: true,
proxyUrl: 'https://localhost:1212',
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, TestUrl);

const res = await request({
axios,
url: 'http://testProxy',
url: TestUrl,
logger,
configurationUtilities,
});

expect(axiosMock).toHaveBeenCalledWith('http://testProxy', {
expect(axiosMock).toHaveBeenCalledWith(TestUrl, {
method: 'get',
data: {},
httpAgent,
Expand All @@ -94,6 +98,8 @@ describe('request', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope:',
proxyRejectUnauthorizedCertificates: false,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});
const res = await request({
axios,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const request = async <T = unknown>({
validateStatus?: (status: number) => boolean;
auth?: AxiosBasicCredentials;
}): Promise<AxiosResponse> => {
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url);

return await axios(url, {
...rest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../../actions_config.mock';
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;

const targetUrl = 'https://elastic.co/foo/bar/baz';

describe('getCustomAgents', () => {
const configurationUtilities = actionsConfigMock.create();

test('get agents for valid proxy URL', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
proxyRejectUnauthorizedCertificates: false,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
expect(httpAgent instanceof HttpProxyAgent).toBeTruthy();
expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy();
});
Expand All @@ -31,14 +35,16 @@ describe('getCustomAgents', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope: not a valid URL',
proxyRejectUnauthorizedCertificates: false,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
expect(httpAgent).toBe(undefined);
expect(httpsAgent instanceof HttpsAgent).toBeTruthy();
});

test('return default agents for undefined proxy options', () => {
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
expect(httpAgent).toBe(undefined);
expect(httpsAgent instanceof HttpsAgent).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ interface GetCustomAgentsResponse {

export function getCustomAgents(
configurationUtilities: ActionsConfigurationUtilities,
logger: Logger
logger: Logger,
url: string
): GetCustomAgentsResponse {
const proxySettings = configurationUtilities.getProxySettings();
const defaultAgents = {
Expand All @@ -33,6 +34,28 @@ export function getCustomAgents(
return defaultAgents;
}

let targetUrl: URL;
try {
targetUrl = new URL(url);
} catch (err) {
logger.warn(`error determining proxy state for invalid url "${url}", using default agents`);
return defaultAgents;
}

// filter out hostnames in the proxy bypass or only lists
const { hostname } = targetUrl;

if (proxySettings.proxyBypassHosts) {
if (proxySettings.proxyBypassHosts.has(hostname)) {
return defaultAgents;
}
}

if (proxySettings.proxyOnlyHosts) {
if (!proxySettings.proxyOnlyHosts.has(hostname)) {
return defaultAgents;
}
}
logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`);
let proxyUrl: URL;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ describe('send_email module', () => {
{
proxyUrl: 'https://example.com',
proxyRejectUnauthorizedCertificates: false,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ describe('execute()', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
proxyRejectUnauthorizedCertificates: false,
proxyBypassHosts: undefined,
proxyOnlyHosts: undefined,
});
const actionTypeProxy = getActionType({
logger: mockedLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ async function slackExecutor(
const { message } = params;
const proxySettings = configurationUtilities.getProxySettings();

const customAgents = getCustomAgents(configurationUtilities, logger);
const customAgents = getCustomAgents(configurationUtilities, logger, webhookUrl);
const agent = webhookUrl.toLowerCase().startsWith('https')
? customAgents.httpsAgent
: customAgents.httpAgent;
Expand Down
59 changes: 36 additions & 23 deletions x-pack/plugins/actions/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,45 @@ const preconfiguredActionSchema = schema.object({
secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
});

export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
allowedHosts: schema.arrayOf(
schema.oneOf([schema.string({ hostname: true }), schema.literal(AllowedHosts.Any)]),
{
defaultValue: [AllowedHosts.Any],
}
),
enabledActionTypes: schema.arrayOf(
schema.oneOf([schema.string(), schema.literal(EnabledActionTypes.Any)]),
{
defaultValue: [AllowedHosts.Any],
}
),
preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, {
defaultValue: {},
validate: validatePreconfigured,
}),
proxyUrl: schema.maybe(schema.string()),
proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())),
proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }),
rejectUnauthorized: schema.boolean({ defaultValue: true }),
});
export const configSchema = schema.object(
{
enabled: schema.boolean({ defaultValue: true }),
allowedHosts: schema.arrayOf(
schema.oneOf([schema.string({ hostname: true }), schema.literal(AllowedHosts.Any)]),
{
defaultValue: [AllowedHosts.Any],
}
),
enabledActionTypes: schema.arrayOf(
schema.oneOf([schema.string(), schema.literal(EnabledActionTypes.Any)]),
{
defaultValue: [AllowedHosts.Any],
}
),
preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, {
defaultValue: {},
validate: validatePreconfigured,
}),
proxyUrl: schema.maybe(schema.string()),
proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())),
proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }),
proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
rejectUnauthorized: schema.boolean({ defaultValue: true }),
},
{ validate }
);

export type ActionsConfig = TypeOf<typeof configSchema>;

function validate(actionsConfig_: unknown): string | undefined {
const actionsConfig = actionsConfig_ as ActionsConfig;

if (actionsConfig.proxyBypassHosts && actionsConfig.proxyOnlyHosts) {
return `properties proxyBypassHosts and proxyOnlyHosts cannot be used together`;
}
}

const invalidActionIds = new Set(['', '__proto__', 'constructor']);

function validatePreconfigured(preconfigured: Record<string, unknown>): string | undefined {
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export interface ActionTaskExecutorParams {

export interface ProxySettings {
proxyUrl: string;
proxyBypassHosts: Set<string> | undefined;
proxyOnlyHosts: Set<string> | undefined;
proxyHeaders?: Record<string, string>;
proxyRejectUnauthorizedCertificates: boolean;
}

0 comments on commit 790736e

Please sign in to comment.