Collect Akamai Enterprise Application Access logs
Parser Version: 3.0
This document explains how to ingest Akamai Enterprise Application Access (EAA) logs to Google Security Operations using Akamai Unified Log Streamer (ULS) and Bindplane. Akamai EAA is a zero-trust network access solution that provides secure, identity-aware access to enterprise applications without a traditional VPN. EAA produces operational data in the form of access logs, admin audit logs, connector health metrics, device inventory, and directory health information. The parser extracts fields from the JSON logs, performs data transformations such as string conversions and IP address extraction, and maps these fields to the UDM, handling various event types including NETWORK_HTTP and USER_UNCATEGORIZED based on the presence of specific fields.
Before you begin
Make sure you have the following prerequisites:
- A Google SecOps instance.
- A Windows Server 2016 or later, or Linux host with
systemdto run the Bindplane agent. - Linux host with Python 3.9 or later, or a containerized environment (Docker or Kubernetes) to run Unified Log Streamer.
- If running behind a proxy, ensure firewall ports are open per the Bindplane agent requirements.
- Akamai EAA tenant with administrative access.
Akamai API credentials:
- For access and admin audit feeds: EAA Legacy API Key and Secret (created in Enterprise Center).
- For connector health, device inventory, and directory health feeds: Akamai OPEN API credentials (EdgeGrid authentication) with READ-WRITE access to the Enterprise Application Access API.
Get Google SecOps ingestion authentication file
- Sign in to the Google SecOps console.
- Go to SIEM Settings > Collection Agents.
- Download the Ingestion Authentication File. Save the file securely on the system where Bindplane will be installed.
Get Google SecOps customer ID
- Sign in to the Google SecOps console.
- Go to SIEM Settings > Profile.
- Copy and save the Customer ID from the Organization Details section.
Install the Bindplane agent
Install the Bindplane agent on your Windows or Linux operating system according to the following instructions.
Windows installation
- Open Command Prompt or PowerShell as an administrator.
Run the following command:
msiexec /i "https://github.com/observIQ/bindplane-agent/releases/latest/download/observiq-otel-collector.msi" /quiet
Linux installation
- Open a terminal with root or sudo privileges.
Run the following command:
sudo sh -c "$(curl -fsSlL https://github.com/observiq/bindplane-agent/releases/latest/download/install_unix.sh)" install_unix.sh
Additional installation resources
For additional installation options, see Bindplane agent installation guide.
Configure the Bindplane agent to ingest logs and send to Google SecOps
Open the Bindplane agent configuration file in a text editor:
Linux:
sudo nano /status observiq-otel-collector/config.yamlWindows:
notepad "C:\Program Files\observIQ OpenTelemetry Collector\config.yaml"Edit the
config.yamlfile as follows:receivers: tcplog: listen_address: "0.0.0.0:5140" exporters: chronicle/akamai_eaa: compression: gzip creds_file_path: '/path/to/ingestion-authentication-file.json' customer_id: '<CUSTOMER_ID>' endpoint: malachiteingestion-pa.googleapis.com log_type: 'AKAMAI_EAA' raw_log_field: body ingestion_labels: source: akamai_eaa service: pipelines: logs/akamai_eaa: receivers: - tcplog exporters: - chronicle/akamai_eaa
Replace the following:
/path/to/ingestion-authentication-file.json: The full path to the ingestion authentication file.- Linux:
/etc/bindplane-agent/ingestion-auth.json - Windows:
C:\Program Files\observIQ OpenTelemetry Collector\ingestion-auth.json
- Linux:
<CUSTOMER_ID>: The Google SecOps customer ID.0.0.0.0:5140: The IP address and port for Bindplane to listen on. Adjust as needed for your environment.
Restart the Bindplane agent to apply the changes
To restart the Bindplane agent in Linux:
Run the following command:
sudo systemctl restart observiq-otel-collectorVerify the service is running:
sudo systemctl status observiq-otel-collectorCheck logs for errors:
sudo journalctl -u observiq-otel-collector -f
To restart the Bindplane agent in Windows:
Choose one of the following options:
- Command Prompt or PowerShell as administrator:
net stop observiq-otel-collector && net start observiq-otel-collector- Services console:
- Press
Win+R, typeservices.msc, and press Enter. - Locate observIQ OpenTelemetry Collector.
- Right-click and select Restart.
- Press
Verify the service is running:
sc query observiq-otel-collectorCheck logs for errors:
type "C:\Program Files\observIQ OpenTelemetry Collector\log\collector.log"
Create Akamai API credentials
Akamai EAA uses two types of API credentials depending on the feed type. Create the credentials that correspond to the feeds you want to collect.
Create EAA Legacy API credentials (for access and admin feeds)
- Sign in to Akamai Control Center.
- Select Enterprise Center from the main navigation menu on the left.
- Go to General Settings > Settings.
- Select the API Keys tab.
- Click Generate new API Key in the top right corner.
- Enter a name and a description for the API key.
On the confirmation screen, copy the Key and the Secret.
Create Akamai OPEN API credentials (for connector health, device inventory, and directory health feeds)
- Sign in to Akamai Control Center.
- Go to Account Admin > Identity and access.
- Select the API Users tab.
- Click Create API Client.
- In the Select APIs section, search for Enterprise Application Access and set the access level to READ-WRITE.
- Complete the remaining settings and click Create API client.
Click Download to download the credentials in
.edgercformat, or click Copy to copy them to an existing.edgercfile.
Configure the .edgerc credentials file
Create the
.edgerccredentials file on the system where ULS will run:touch ~/.edgercOpen the file in a text editor:
nano ~/.edgercAdd your Akamai API credentials in the following format:
[default] ; EAA Legacy API credentials (for access and admin feeds) eaa_api_host = manage.akamai-access.com eaa_api_key = <EAA_API_KEY> eaa_api_secret = <EAA_API_SECRET> ; Akamai OPEN API credentials (for conhealth, devinv, dirhealth feeds) host = <OPEN_API_HOST> client_token = <CLIENT_TOKEN> client_secret = <CLIENT_SECRET> access_token = <ACCESS_TOKEN>Secure the credentials file:
chmod 600 ~/.edgerc
Replace the following:
<EAA_API_KEY>: The EAA Legacy API Key (for example,XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).<EAA_API_SECRET>: The EAA Legacy API Secret.<OPEN_API_HOST>: Your Akamai OPEN API hostname (for example,akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net).<CLIENT_TOKEN>: Your Akamai OPEN API client token (for example,akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx).<CLIENT_SECRET>: Your Akamai OPEN API client secret.<ACCESS_TOKEN>: Your Akamai OPEN API access token (for example,akab-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx).
Install Akamai Unified Log Streamer
Unified Log Streamer (ULS) pulls logs from Akamai EAA via the Akamai APIs and streams them to the Bindplane agent using TCP. ULS is a Python application that requires Python 3.9 or later.
Linux installation (Python)
Clone the ULS repository:
git clone https://github.com/akamai/uls.git cd ulsInstall ULS Python requirements:
pip3 install -r bin/requirements.txtInstall the EAA CLI dependency:
git clone --depth 1 --single-branch https://github.com/akamai/cli-eaa.git ext/cli-eaa pip3 install -r ext/cli-eaa/requirements.txtVerify the installation:
python3 bin/uls.py --help
Docker installation
Pull the official ULS Docker image:
docker pull akamai/uls:latest
Configure ULS to stream EAA logs to Bindplane
Command-line execution (testing)
Run ULS with TCP output to stream logs to the Bindplane agent:
python3 bin/uls.py --input eaa \ --feed access \ --output tcp \ --host <BINDPLANE_HOST> \ --port 5140 \ --edgerc ~/.edgerc \ --section default
Replace the following:
<BINDPLANE_HOST>: The IP address or hostname of the server running the Bindplane agent.5140: The port configured in the Bindplanetcplogreceiver.To stream multiple feed types, run separate ULS instances:
# Access logs python3 bin/uls.py --input eaa --feed access --output tcp --host <BINDPLANE_HOST> --port 5140 --edgerc ~/.edgerc --section default # Admin audit logs python3 bin/uls.py --input eaa --feed admin --output tcp --host <BINDPLANE_HOST> --port 5140 --edgerc ~/.edgerc --section default # Connector health python3 bin/uls.py --input eaa --feed conhealth --output tcp --host <BINDPLANE_HOST> --port 5140 --edgerc ~/.edgerc --section default
Systemd service (production)
For production deployments, configure ULS as a systemd service.
Create a ULS configuration file:
sudo mkdir -p /etc/uls sudo nano /etc/uls/eaa-access-tcp.confAdd the following configuration:
ULS_INPUT=eaa ULS_FEED=access ULS_OUTPUT=tcp ULS_HOST=<BINDPLANE_HOST> ULS_PORT=5140 ULS_EDGERC=/root/.edgerc ULS_SECTION=defaultCreate a systemd service file:
sudo nano /etc/systemd/system/uls-eaa-access.serviceAdd the following content:
[Unit] Description=Unified Log Streamer - EAA Access Logs to Bindplane After=network.target [Service] Type=simple EnvironmentFile=/etc/uls/eaa-access-tcp.conf ExecStart=/usr/bin/python3 /opt/uls/bin/uls.py --input ${ULS_INPUT} --feed ${ULS_FEED} --output ${ULS_OUTPUT} --host ${ULS_HOST} --port ${ULS_PORT} --edgerc ${ULS_EDGERC} --section ${ULS_SECTION} Restart=always RestartSec=10 User=root [Install] WantedBy=multi-user.targetEnable and start the service:
sudo systemctl daemon-reload sudo systemctl enable uls-eaa-access.service sudo systemctl start uls-eaa-access.serviceVerify the service is running:
sudo systemctl status uls-eaa-access.serviceView logs:
sudo journalctl -u uls-eaa-access.service -f
Repeat steps 1-7 for each additional feed type (admin, conhealth) by creating separate configuration and service files with different names (for example, uls-eaa-admin.service, uls-eaa-conhealth.service).
Docker Compose deployment
Create a
docker-compose.ymlfile:version: '3.8' services: uls-eaa-access: image: akamai/uls:latest container_name: uls-eaa-access restart: unless-stopped environment: - ULS_INPUT=eaa - ULS_FEED=access - ULS_OUTPUT=tcp - ULS_HOST=<BINDPLANE_HOST> - ULS_PORT=5140 volumes: - ~/.edgerc:/root/.edgerc:ro uls-eaa-admin: image: akamai/uls:latest container_name: uls-eaa-admin restart: unless-stopped environment: - ULS_INPUT=eaa - ULS_FEED=admin - ULS_OUTPUT=tcp - ULS_HOST=<BINDPLANE_HOST> - ULS_PORT=5140 volumes: - ~/.edgerc:/root/.edgerc:ro uls-eaa-conhealth: image: akamai/uls:latest container_name: uls-eaa-conhealth restart: unless-stopped environment: - ULS_INPUT=eaa - ULS_FEED=conhealth - ULS_OUTPUT=tcp - ULS_HOST=<BINDPLANE_HOST> - ULS_PORT=5140 volumes: - ~/.edgerc:/root/.edgerc:ro
Replace <BINDPLANE_HOST> with the IP address or hostname of the Bindplane agent server.
Start the containers:
docker compose up -dView logs:
docker compose logs -f
UDM mapping table
| Log field | UDM mapping | Logic |
|---|---|---|
app |
target.application |
The value after the colon in the app field. |
apphost |
target.hostname |
Directly mapped. |
browser |
network.http.user_agent |
Directly mapped. |
bytes_in |
network.recieved_bytes |
Directly mapped. |
bytes_out |
network.sent_bytes |
Directly mapped. |
cc |
principal.location.country_or_region |
Directly mapped. |
client_id |
additional.fields.key: "Client Id", additional.fields.value.string_value: client_id |
Conditionally mapped if client_id is present. |
clientip |
principal.ip |
Directly mapped. |
cloud_zone |
principal.cloud.availability_zone |
Directly mapped. |
connector_resp_time |
security_result.detection_fields.key: "Connector response time", security_result.detection_fields.value: connector_resp_time |
Conditionally mapped if connector_resp_time is not empty or "-". |
content_type |
additional.fields.key: "Content type", additional.fields.value.string_value: content_type |
Conditionally mapped if content_type is present. |
datetime |
metadata.event_timestamp |
Parsed from the datetime field using the RFC 3339 format. |
deny_reason |
security_result.summary |
Directly mapped. |
device_type |
principal.platform, principal.platform_version |
Mapped to WINDOWS, LINUX, or MAC based on regex matching. The raw value is mapped to principal.platform_version. |
di |
metadata.ingestion_labels.key: "di", metadata.ingestion_labels.value: di |
Directly mapped as an ingestion label. |
error_code |
additional.fields.key: "Error code", additional.fields.value.string_value: error_code |
Conditionally mapped if error_code is present. |
event |
metadata.description |
Directly mapped. |
geo_city |
principal.location.city |
Directly mapped. |
geo_country |
principal.location.country_or_region |
Directly mapped. |
geo_state |
principal.location.state |
Directly mapped. |
groups |
principal.user.group_identifiers |
Directly mapped. |
http_method |
network.http.method |
Directly mapped. |
http_ver |
network.application_protocol, network.application_protocol_version |
Parsed using grok to extract protocol and version. |
idpinfo |
additional.fields.key: "IDP Info", additional.fields.value.string_value: idpinfo |
Conditionally mapped if idpinfo is present. |
internal_host |
additional.fields.key: "Internal host", additional.fields.value.string_value: internal_host |
Conditionally mapped if internal_host is present. |
metadata.log_type |
metadata.log_type |
Hardcoded to "AKAMAI_EAA". |
metadata.product_name |
metadata.product_name |
Hardcoded to "AKAMAI_EAA". |
metadata.vendor_name |
metadata.vendor_name |
Hardcoded to "AKAMAI_EAA". |
metadata.event_type |
metadata.event_type |
Determined by logic: USER_UNCATEGORIZED if uid is present, NETWORK_HTTP if both principal.ip and target are set, or GENERIC_EVENT otherwise. |
origin_host |
additional.fields.key: "Origin host", additional.fields.value.string_value: origin_host |
Conditionally mapped if origin_host is present. |
origin_resp_time |
security_result.detection_fields.key: "Origin response time", security_result.detection_fields.value: origin_resp_time |
Conditionally mapped if origin_resp_time is not empty or "-". |
os |
principal.platform |
Mapped to WINDOWS, MAC, or LINUX based on regex matching. |
port |
target.port |
The value after the colon in the app field. |
ral |
metadata.description |
Concatenated values of the ral array, separated by commas. |
referer |
network.http.referral_url |
Directly mapped. |
resource |
principal.resource.attribute.labels.key: "Resource", principal.resource.attribute.labels.value: resource |
Conditionally mapped if resource is present. |
resource_type |
principal.resource.attribute.labels.key: "Resource Type", principal.resource.attribute.labels.value: resource_type |
Conditionally mapped if resource_type is present. |
rscd |
metadata.ingestion_labels.key: "rscd", metadata.ingestion_labels.value: rscd |
Directly mapped as an ingestion label. |
session_id |
network.session_id |
Directly mapped. |
session_info |
additional.fields.key: "Session info", additional.fields.value.string_value: session_info |
Conditionally mapped if session_info is present. |
state |
principal.location.state |
Directly mapped. |
status_code |
network.http.response_code |
Directly mapped. |
total_resp_time |
security_result.detection_fields.key: "Total response time", security_result.detection_fields.value: total_resp_time |
Conditionally mapped if total_resp_time is not empty or "-". |
ts |
metadata.event_timestamp |
Parsed from the ts field as UNIX milliseconds or seconds if present, otherwise from the datetime field. |
uid |
principal.user.userid |
Directly mapped. |
uip |
principal.ip |
Directly mapped. |
url_path |
target.url |
Directly mapped. |
user_agent |
network.http.user_agent, network.http.parsed_user_agent |
Directly mapped and parsed into a structured parsed_user_agent field. |
username |
principal.user.email_addresses or principal.user.userid |
Mapped to email_addresses if it looks like an email, otherwise to userid. |
Change Log
View the Change Log for this parser
Need more help? Get answers from Community members and Google SecOps professionals.