Skip to content

Commit

Permalink
Include client IP address in audit log (#147526)
Browse files Browse the repository at this point in the history
Resolves #127481

## Release notes

Include IP address in audit log

## Testing

1. Update `kibana.dev.yaml`:

```yaml
xpack.security.audit.enabled: true
xpack.security.audit.appender:
  type: console
  layout:
    type: json
```

2. Observe audit logs in console when interacting with Kibana:

```json
{
  "@timestamp": "2022-12-13T15:50:42.236+00:00",
  "message": "User is requesting [/dev/internal/security/me] endpoint",
  "client": {
    "ip": "127.0.0.1"
  },
  "http": {
    "request": {
      "headers": {
        "x-forwarded-for": "1.1.1.1, 127.0.0.1"
      }
    }
  }
}
```

Note: You will see the `x-forwarded-for` field populated when running
Kibana in development mode (`yarn start`) since Kibana runs behind a
development proxy.

Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
  • Loading branch information
thomheymann and gchaps committed Dec 16, 2022
1 parent a2036f0 commit a02c7dc
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 44 deletions.
8 changes: 8 additions & 0 deletions docs/user/security/audit-logging.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,19 @@ Example: `[marketing]`
| *Field*
| *Description*

| `client.ip`
| Client IP address.

| `http.request.method`
| HTTP request method.

Example: `get`, `post`, `put`, `delete`

| `http.request.headers.x-forwarded-for`
| `X-Forwarded-For` request header used to identify the originating client IP address when connecting through proxy servers.

Example: `161.66.20.177, 236.198.214.101`

| `url.domain`
| Domain of the URL.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,11 @@ describe('KibanaSocket', () => {
expect(socket.authorizationError).toBe(authorizationError);
});
});

describe('remoteAddress', () => {
it('mirrors the value of net.Socket instance', () => {
const socket = new KibanaSocket({ remoteAddress: '1.1.1.1' } as Socket);
expect(socket.remoteAddress).toBe('1.1.1.1');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export class KibanaSocket implements IKibanaSocket {
return this.socket instanceof TLSSocket ? this.socket.authorizationError : undefined;
}

public get remoteAddress() {
return this.socket.remoteAddress;
}

constructor(private readonly socket: Socket) {}

getPeerCertificate(detailed: true): DetailedPeerCertificate | null;
Expand Down
7 changes: 7 additions & 0 deletions packages/core/http/core-http-server/src/router/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ export interface IKibanaSocket {
* only when `authorized` is `false`.
*/
readonly authorizationError?: Error;

/**
* The string representation of the remote IP address. For example,`'74.125.127.100'` or
* `'2001:4860:a005::68'`. Value may be `undefined` if the socket is destroyed (for example, if
* the client disconnected).
*/
readonly remoteAddress?: string;
}
109 changes: 67 additions & 42 deletions x-pack/plugins/security/server/audit/audit_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,71 @@ import type { EcsEventOutcome, EcsEventType, KibanaRequest, LogMeta } from '@kbn
import type { AuthenticationProvider } from '../../common/model';
import type { AuthenticationResult } from '../authentication/authentication_result';

/**
* Audit kibana schema using ECS format
*/
export interface AuditKibana {
/**
* The ID of the space associated with this event.
*/
space_id?: string;
/**
* The ID of the user session associated with this event. Each login attempt
* results in a unique session id.
*/
session_id?: string;
/**
* Saved object that was created, changed, deleted or accessed as part of this event.
*/
saved_object?: {
type: string;
id: string;
};
/**
* Name of authentication provider associated with a login event.
*/
authentication_provider?: string;
/**
* Type of authentication provider associated with a login event.
*/
authentication_type?: string;
/**
* Name of Elasticsearch realm that has authenticated the user.
*/
authentication_realm?: string;
/**
* Name of Elasticsearch realm where the user details were retrieved from.
*/
lookup_realm?: string;
/**
* Set of space IDs that a saved object was shared to.
*/
add_to_spaces?: readonly string[];
/**
* Set of space IDs that a saved object was removed from.
*/
delete_from_spaces?: readonly string[];
}

type EcsHttp = Required<LogMeta>['http'];
type EcsRequest = Required<EcsHttp>['request'];

/**
* Audit request schema using ECS format
*/
export interface AuditRequest extends EcsRequest {
headers?: {
'x-forwarded-for'?: string;
};
}

/**
* Audit http schema using ECS format
*/
export interface AuditHttp extends EcsHttp {
request?: AuditRequest;
}

/**
* Audit event schema using ECS format: https://www.elastic.co/guide/en/ecs/1.12/index.html
*
Expand All @@ -24,48 +89,8 @@ import type { AuthenticationResult } from '../authentication/authentication_resu
*/
export interface AuditEvent extends LogMeta {
message: string;
kibana?: {
/**
* The ID of the space associated with this event.
*/
space_id?: string;
/**
* The ID of the user session associated with this event. Each login attempt
* results in a unique session id.
*/
session_id?: string;
/**
* Saved object that was created, changed, deleted or accessed as part of this event.
*/
saved_object?: {
type: string;
id: string;
};
/**
* Name of authentication provider associated with a login event.
*/
authentication_provider?: string;
/**
* Type of authentication provider associated with a login event.
*/
authentication_type?: string;
/**
* Name of Elasticsearch realm that has authenticated the user.
*/
authentication_realm?: string;
/**
* Name of Elasticsearch realm where the user details were retrieved from.
*/
lookup_realm?: string;
/**
* Set of space IDs that a saved object was shared to.
*/
add_to_spaces?: readonly string[];
/**
* Set of space IDs that a saved object was removed from.
*/
delete_from_spaces?: readonly string[];
};
kibana?: AuditKibana;
http?: AuditHttp;
}

export interface HttpRequestParams {
Expand Down
44 changes: 42 additions & 2 deletions x-pack/plugins/security/server/audit/audit_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import type { Socket } from 'net';
import { lastValueFrom, Observable, of } from 'rxjs';

import {
Expand All @@ -22,6 +23,7 @@ import {
AuditService,
createLoggingConfig,
filterEvent,
getForwardedFor,
RECORD_USAGE_INTERVAL,
} from './audit_service';

Expand Down Expand Up @@ -186,14 +188,26 @@ describe('#asScoped', () => {
recordAuditLoggingUsage,
});
const request = httpServerMock.createKibanaRequest({
socket: { remoteAddress: '3.3.3.3' } as Socket,
headers: {
'x-forwarded-for': '1.1.1.1, 2.2.2.2',
},
kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' },
});

await auditSetup.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } });
expect(logger.info).toHaveBeenCalledWith('MESSAGE', {
await auditSetup.asScoped(request).log({
message: 'MESSAGE',
event: { action: 'ACTION' },
http: { request: { method: 'GET' } },
});
expect(logger.info).toHaveBeenLastCalledWith('MESSAGE', {
event: { action: 'ACTION' },
kibana: { space_id: 'default', session_id: 'SESSION_ID' },
trace: { id: 'REQUEST_ID' },
client: { ip: '3.3.3.3' },
http: {
request: { method: 'GET', headers: { 'x-forwarded-for': '1.1.1.1, 2.2.2.2' } },
},
user: { id: 'uid', name: 'jdoe', roles: ['admin'] },
});
audit.stop();
Expand Down Expand Up @@ -424,6 +438,32 @@ describe('#createLoggingConfig', () => {
});
});

describe('#getForwardedFor', () => {
it('extracts x-forwarded-for header from request', () => {
const request = httpServerMock.createKibanaRequest({
headers: {
'x-forwarded-for': '1.1.1.1',
},
});
expect(getForwardedFor(request)).toBe('1.1.1.1');
});

it('concatenates multiple headers into single string in correct order', () => {
const request = httpServerMock.createKibanaRequest({
headers: {
// @ts-expect-error Headers can be arrays but HAPI mocks are incorrectly typed
'x-forwarded-for': ['1.1.1.1, 2.2.2.2', '3.3.3.3'],
},
});
expect(getForwardedFor(request)).toBe('1.1.1.1, 2.2.2.2, 3.3.3.3');
});

it('returns undefined when header not present', () => {
const request = httpServerMock.createKibanaRequest();
expect(getForwardedFor(request)).toBeUndefined();
});
});

describe('#filterEvent', () => {
let event: AuditEvent;

Expand Down
27 changes: 27 additions & 0 deletions x-pack/plugins/security/server/audit/audit_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export class AuditService {
const spaceId = getSpaceId(request);
const user = getCurrentUser(request);
const sessionId = await getSID(request);
const forwardedFor = getForwardedFor(request);

log({
...event,
user:
Expand All @@ -177,6 +179,18 @@ export class AuditService {
...event.kibana,
},
trace: { id: request.id },
client: { ip: request.socket.remoteAddress },
http: forwardedFor
? {
...event.http,
request: {
...event.http?.request,
headers: {
'x-forwarded-for': forwardedFor,
},
},
}
: event.http,
});
},
enabled,
Expand Down Expand Up @@ -243,3 +257,16 @@ export function filterEvent(
}
return true;
}

/**
* Extracts `X-Forwarded-For` header(s) from `KibanaRequest`.
*/
export function getForwardedFor(request: KibanaRequest) {
const forwardedFor = request.headers['x-forwarded-for'];

if (Array.isArray(forwardedFor)) {
return forwardedFor.join(', ');
}

return forwardedFor;
}
6 changes: 6 additions & 0 deletions x-pack/test/security_api_integration/tests/audit/audit_log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function ({ getService }: FtrProviderContext) {
await supertest
.post('/internal/security/login')
.set('kbn-xsrf', 'xxx')
.set('X-Forwarded-For', '1.1.1.1, 2.2.2.2')
.send({
providerType: 'basic',
providerName: 'basic',
Expand All @@ -71,12 +72,15 @@ export default function ({ getService }: FtrProviderContext) {
expect(loginEvent.event.outcome).to.be('success');
expect(loginEvent.trace.id).to.be.ok();
expect(loginEvent.user.name).to.be(username);
expect(loginEvent.client.ip).to.be.ok();
expect(loginEvent.http.request.headers['x-forwarded-for']).to.be('1.1.1.1, 2.2.2.2');
});

it('logs audit events when failing to log in', async () => {
await supertest
.post('/internal/security/login')
.set('kbn-xsrf', 'xxx')
.set('X-Forwarded-For', '1.1.1.1, 2.2.2.2')
.send({
providerType: 'basic',
providerName: 'basic',
Expand All @@ -93,6 +97,8 @@ export default function ({ getService }: FtrProviderContext) {
expect(loginEvent.event.outcome).to.be('failure');
expect(loginEvent.trace.id).to.be.ok();
expect(loginEvent.user).not.to.be.ok();
expect(loginEvent.client.ip).to.be.ok();
expect(loginEvent.http.request.headers['x-forwarded-for']).to.be('1.1.1.1, 2.2.2.2');
});
});
}

0 comments on commit a02c7dc

Please sign in to comment.