mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Fleet] sign unenroll action (#160932)
This commit is contained in:
parent
a17e85ec15
commit
2096290ac1
7 changed files with 242 additions and 48 deletions
|
@ -407,5 +407,11 @@ export interface FleetServerAgentAction {
|
|||
/** Trace id */
|
||||
traceparent?: string | null;
|
||||
|
||||
// signed data + signature
|
||||
signed?: {
|
||||
data: string;
|
||||
signature: string;
|
||||
};
|
||||
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
|
|
@ -178,8 +178,13 @@ export function createMessageSigningServiceMock(): MessageSigningServiceInterfac
|
|||
return {
|
||||
isEncryptionAvailable: true,
|
||||
generateKeyPair: jest.fn(),
|
||||
sign: jest.fn(),
|
||||
getPublicKey: jest.fn(),
|
||||
sign: jest.fn().mockImplementation((message: Record<string, unknown>) =>
|
||||
Promise.resolve({
|
||||
data: Buffer.from(JSON.stringify(message), 'utf8'),
|
||||
signature: 'thisisasignature',
|
||||
})
|
||||
),
|
||||
getPublicKey: jest.fn().mockResolvedValue('thisisapublickey'),
|
||||
rotateKeyPair: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -438,19 +438,7 @@ describe('getFullAgentPolicy', () => {
|
|||
});
|
||||
|
||||
it('should populate agent.protection and signed properties if encryption is available', async () => {
|
||||
const mockContext = createAppContextStartContractMock();
|
||||
mockContext.messageSigningService.sign = jest
|
||||
.fn()
|
||||
.mockImplementation((message: Record<string, unknown>) =>
|
||||
Promise.resolve({
|
||||
data: Buffer.from(JSON.stringify(message), 'utf8'),
|
||||
signature: 'thisisasignature',
|
||||
})
|
||||
);
|
||||
mockContext.messageSigningService.getPublicKey = jest
|
||||
.fn()
|
||||
.mockResolvedValue('thisisapublickey');
|
||||
appContextService.start(mockContext);
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
|
||||
mockAgentPolicy({});
|
||||
const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy');
|
||||
|
@ -461,7 +449,7 @@ describe('getFullAgentPolicy', () => {
|
|||
signing_key: 'thisisapublickey',
|
||||
});
|
||||
expect(agentPolicy!.signed).toMatchObject({
|
||||
data: 'eyJpZCI6ImFnZW50LXBvbGljeSIsImFnZW50Ijp7InByb3RlY3Rpb24iOnsiZW5hYmxlZCI6ZmFsc2UsInVuaW5zdGFsbF90b2tlbl9oYXNoIjoiIiwic2lnbmluZ19rZXkiOiJ0aGlzaXNhcHVibGlja2V5In19fQ==',
|
||||
data: 'eyJpZCI6ImFnZW50LXBvbGljeSIsImFnZW50Ijp7ImZlYXR1cmVzIjp7fSwicHJvdGVjdGlvbiI6eyJlbmFibGVkIjpmYWxzZSwidW5pbnN0YWxsX3Rva2VuX2hhc2giOiIiLCJzaWduaW5nX2tleSI6InRoaXNpc2FwdWJsaWNrZXkifX0sImlucHV0cyI6W119',
|
||||
signature: 'thisisasignature',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -89,6 +89,15 @@ export async function getFullAgentPolicy(
|
|||
})
|
||||
);
|
||||
|
||||
const inputs = await storedPackagePoliciesToAgentInputs(
|
||||
agentPolicy.package_policies as PackagePolicy[],
|
||||
packageInfoCache,
|
||||
getOutputIdForAgentPolicy(dataOutput)
|
||||
);
|
||||
const features = (agentPolicy.agent_features || []).reduce((acc, { name, ...featureConfig }) => {
|
||||
acc[name] = featureConfig;
|
||||
return acc;
|
||||
}, {} as NonNullable<FullAgentPolicy['agent']>['features']);
|
||||
const fullAgentPolicy: FullAgentPolicy = {
|
||||
id: agentPolicy.id,
|
||||
outputs: {
|
||||
|
@ -102,11 +111,7 @@ export async function getFullAgentPolicy(
|
|||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
inputs: await storedPackagePoliciesToAgentInputs(
|
||||
agentPolicy.package_policies as PackagePolicy[],
|
||||
packageInfoCache,
|
||||
getOutputIdForAgentPolicy(dataOutput)
|
||||
),
|
||||
inputs,
|
||||
secret_references: (agentPolicy?.package_policies || []).flatMap(
|
||||
(policy) => policy.secret_references || []
|
||||
),
|
||||
|
@ -125,10 +130,7 @@ export async function getFullAgentPolicy(
|
|||
metrics: agentPolicy.monitoring_enabled.includes(dataTypes.Metrics),
|
||||
}
|
||||
: { enabled: false, logs: false, metrics: false },
|
||||
features: (agentPolicy.agent_features || []).reduce((acc, { name, ...featureConfig }) => {
|
||||
acc[name] = featureConfig;
|
||||
return acc;
|
||||
}, {} as NonNullable<FullAgentPolicy['agent']>['features']),
|
||||
features,
|
||||
protection: {
|
||||
enabled: agentPolicy.is_protected,
|
||||
uninstall_token_hash: '',
|
||||
|
@ -206,8 +208,15 @@ export async function getFullAgentPolicy(
|
|||
const dataToSign = {
|
||||
id: fullAgentPolicy.id,
|
||||
agent: {
|
||||
features,
|
||||
protection: fullAgentPolicy.agent.protection,
|
||||
},
|
||||
inputs: inputs.map(({ id: inputId, name, revision, type }) => ({
|
||||
id: inputId,
|
||||
name,
|
||||
revision,
|
||||
type,
|
||||
})),
|
||||
};
|
||||
|
||||
const { data: signedData, signature } = await messageSigningService.sign(dataToSign);
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import type { NewAgentAction } from '../../../common/types';
|
||||
import type { NewAgentAction, AgentActionType } from '../../../common/types';
|
||||
|
||||
import { createAppContextStartContractMock } from '../../mocks';
|
||||
import { createAppContextStartContractMock, type MockedFleetAppContext } from '../../mocks';
|
||||
import { appContextService } from '../app_context';
|
||||
import { auditLoggingService } from '../audit_logging';
|
||||
|
||||
|
@ -29,8 +29,11 @@ const mockedBulkUpdateAgents = bulkUpdateAgents as jest.MockedFunction<typeof bu
|
|||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
describe('Agent actions', () => {
|
||||
let mockContext: MockedFleetAppContext;
|
||||
|
||||
beforeEach(async () => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
mockContext = createAppContextStartContractMock();
|
||||
appContextService.start(mockContext);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -66,6 +69,17 @@ describe('Agent actions', () => {
|
|||
});
|
||||
|
||||
describe('createAgentAction', () => {
|
||||
beforeEach(() => {
|
||||
mockContext.messageSigningService.sign = jest
|
||||
.fn()
|
||||
.mockImplementation((message: Record<string, unknown>) =>
|
||||
Promise.resolve({
|
||||
data: Buffer.from(JSON.stringify(message), 'utf8'),
|
||||
signature: 'thisisasignature',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
esClient.search.mockResolvedValue({
|
||||
|
@ -93,6 +107,85 @@ describe('Agent actions', () => {
|
|||
message: expect.stringMatching(/User created Fleet action/),
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['UNENROLL', 'UPGRADE'] as AgentActionType[])(
|
||||
'should sign %s action',
|
||||
async (actionType: AgentActionType) => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
esClient.search.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
type: actionType,
|
||||
action_id: 'action1',
|
||||
agents: ['agent1', 'agent2'],
|
||||
expiration: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
|
||||
await createAgentAction(esClient, {
|
||||
id: 'action1',
|
||||
type: actionType,
|
||||
agents: ['agent1'],
|
||||
});
|
||||
|
||||
expect(esClient.create).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
signed: {
|
||||
data: expect.any(String),
|
||||
signature: expect.any(String),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
'SETTINGS',
|
||||
'POLICY_REASSIGN',
|
||||
'CANCEL',
|
||||
'FORCE_UNENROLL',
|
||||
'UPDATE_TAGS',
|
||||
'REQUEST_DIAGNOSTICS',
|
||||
'POLICY_CHANGE',
|
||||
'INPUT_ACTION',
|
||||
] as AgentActionType[])('should not sign %s action', async (actionType: AgentActionType) => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
esClient.search.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
type: actionType,
|
||||
action_id: 'action1',
|
||||
agents: ['agent1', 'agent2'],
|
||||
expiration: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
|
||||
await createAgentAction(esClient, {
|
||||
id: 'action1',
|
||||
type: actionType,
|
||||
agents: ['agent1'],
|
||||
});
|
||||
|
||||
expect(esClient.create).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.not.objectContaining({
|
||||
signed: expect.any(Object),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateAgentAction', () => {
|
||||
|
@ -125,6 +218,75 @@ describe('Agent actions', () => {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should sign UNENROLL and UPGRADE actions', async () => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
const newActions: NewAgentAction[] = (['UNENROLL', 'UPGRADE'] as AgentActionType[]).map(
|
||||
(actionType, i) => {
|
||||
const actionId = `action${i + 1}`;
|
||||
return {
|
||||
id: actionId,
|
||||
type: actionType,
|
||||
agents: [actionId],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
await bulkCreateAgentActions(esClient, newActions);
|
||||
expect(esClient.bulk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
signed: {
|
||||
data: expect.any(String),
|
||||
signature: expect.any(String),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not sign actions other than UNENROLL and UPGRADE', async () => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
const newActions: NewAgentAction[] = (
|
||||
[
|
||||
'SETTINGS',
|
||||
'POLICY_REASSIGN',
|
||||
'CANCEL',
|
||||
'FORCE_UNENROLL',
|
||||
'UPDATE_TAGS',
|
||||
'REQUEST_DIAGNOSTICS',
|
||||
'POLICY_CHANGE',
|
||||
'INPUT_ACTION',
|
||||
] as AgentActionType[]
|
||||
).map((actionType, i) => {
|
||||
const actionId = `action${i + 1}`;
|
||||
return {
|
||||
id: actionId,
|
||||
type: actionType,
|
||||
agents: [actionId],
|
||||
};
|
||||
});
|
||||
|
||||
await bulkCreateAgentActions(esClient, newActions);
|
||||
expect(esClient.bulk).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
signed: {
|
||||
data: expect.any(String),
|
||||
signature: expect.any(String),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateAgentActionResults', () => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { appContextService } from '../app_context';
|
|||
import type {
|
||||
Agent,
|
||||
AgentAction,
|
||||
AgentActionType,
|
||||
NewAgentAction,
|
||||
FleetServerAgentAction,
|
||||
} from '../../../common/types/models';
|
||||
|
@ -31,6 +32,8 @@ const ONE_MONTH_IN_MS = 2592000000;
|
|||
|
||||
export const NO_EXPIRATION = 'NONE';
|
||||
|
||||
const SIGNED_ACTIONS: Set<Partial<AgentActionType>> = new Set(['UNENROLL', 'UPGRADE']);
|
||||
|
||||
export async function createAgentAction(
|
||||
esClient: ElasticsearchClient,
|
||||
newAgentAction: NewAgentAction
|
||||
|
@ -54,6 +57,15 @@ export async function createAgentAction(
|
|||
traceparent: apm.currentTraceparent,
|
||||
};
|
||||
|
||||
const messageSigningService = appContextService.getMessageSigningService();
|
||||
if (SIGNED_ACTIONS.has(newAgentAction.type) && messageSigningService) {
|
||||
const signedBody = await messageSigningService.sign(body);
|
||||
body.signed = {
|
||||
data: signedBody.data.toString('base64'),
|
||||
signature: signedBody.signature,
|
||||
};
|
||||
}
|
||||
|
||||
await esClient.create({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
id: uuidv4(),
|
||||
|
@ -88,30 +100,41 @@ export async function bulkCreateAgentActions(
|
|||
return [];
|
||||
}
|
||||
|
||||
const messageSigningService = appContextService.getMessageSigningService();
|
||||
await esClient.bulk({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
body: actions.flatMap((action) => {
|
||||
const body: FleetServerAgentAction = {
|
||||
'@timestamp': new Date().toISOString(),
|
||||
expiration: action.expiration ?? new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(),
|
||||
start_time: action.start_time,
|
||||
rollout_duration_seconds: action.rollout_duration_seconds,
|
||||
agents: action.agents,
|
||||
action_id: action.id,
|
||||
data: action.data,
|
||||
type: action.type,
|
||||
traceparent: apm.currentTraceparent,
|
||||
};
|
||||
body: await Promise.all(
|
||||
actions.flatMap(async (action) => {
|
||||
const body: FleetServerAgentAction = {
|
||||
'@timestamp': new Date().toISOString(),
|
||||
expiration: action.expiration ?? new Date(Date.now() + ONE_MONTH_IN_MS).toISOString(),
|
||||
start_time: action.start_time,
|
||||
rollout_duration_seconds: action.rollout_duration_seconds,
|
||||
agents: action.agents,
|
||||
action_id: action.id,
|
||||
data: action.data,
|
||||
type: action.type,
|
||||
traceparent: apm.currentTraceparent,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
create: {
|
||||
_id: action.id,
|
||||
if (SIGNED_ACTIONS.has(action.type) && messageSigningService) {
|
||||
const signedBody = await messageSigningService.sign(body);
|
||||
body.signed = {
|
||||
data: signedBody.data.toString('base64'),
|
||||
signature: signedBody.signature,
|
||||
};
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
create: {
|
||||
_id: action.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
body,
|
||||
];
|
||||
}),
|
||||
body,
|
||||
];
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
for (const action of actions) {
|
||||
|
|
|
@ -21,6 +21,7 @@ jest.mock('../app_context', () => {
|
|||
appContextService: {
|
||||
getLogger: () => loggerMock.create(),
|
||||
getConfig: () => {},
|
||||
getMessageSigningService: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue