Installation

Using the provided Docker images, components can be easily deployed in a local environment or on any cloud platform, such as Microsoft Azure or Amazon AWS.

Database Setup

Mobile In-App Protection backend services use a shared PostgreSQL database instance as the primary data store and a Redis instance for data caching and streaming.

PostgreSQL Setup

The Docker images automatically keep the database schema up-to-date using Liquibase. Whenever you install the application or update it to a new version, the container updates the database schema.

Install PostgreSQL

Download and install the current version of PostgreSQL:

Start the database and connect to it using your preferred database tool.

Create User

Start by creating a user in the database and by setting the user a strong password.

Tip: You can generate a strong password locally on your computer using openssl rand -base64 12.

CREATE USER username;
ALTER USER username WITH PASSWORD '$PASSWORD$';

Create Database

Now, let’s create the iap database to which the data will be stored.

CREATE DATABASE iap;
GRANT ALL PRIVILEGES ON DATABASE iap TO username;

You can assign more granular privileges instead of using ALL PRIVILEGES, if required for security reasons.

Configure Monthly Partitioning

Note: This step only applies to PostgreSQL and is optional based on your partitioning needs.

The iap_app_event, iap_app_fingerprint_history and iap_app_flag_history tables are ever-growing tables holding business data with a potential multi-year retention requirement. The Liquibase changelog creates them as RANGE-partitioned tables (partitioned by timestamp_created) but, for PostgreSQL, only creates a single DEFAULT partition. If, based on your usage pattern / retention requirements / expected amount of data, you do not benefit from partitioning, you can skip this step. Otherwise, use the pg_partman extension to automatically create and retire monthly partitions for these tables to take full advantage of partitioning (pruning, fast bulk deletes, index bloat control).

Install pg_partman

On Debian/Ubuntu, install the package matching your PostgreSQL major version from the PGDG APT repository:

sudo apt install postgresql-${PG_MAJOR_VERSION}-partman

On RHEL/Rocky Linux/AlmaLinux/Oracle Linux, install it from the PGDG YUM repository instead:

sudo dnf install pg_partman_${PG_MAJOR_VERSION}

If you are running a managed database service (e.g. Amazon RDS/Aurora, Azure Database for PostgreSQL Flexible Server, Google Cloud SQL), check the provider’s documentation for enabling pg_partman — it is on the allow-list of most managed offerings, but must be explicitly added to the instance/parameter group before it can be created.

For any other distribution, or if no prebuilt package is available, build the extension from source following the pg_partman installation guide.

Create the extension in a dedicated schema:

CREATE SCHEMA IF NOT EXISTS partman;
CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman;
Configure Partition Sets

Register each of the three tables as a pg_partman-managed partition set.

Note: Perform this step after Database Initialization as the partitioned tables must already exist.

The partitioned tables shall be empty.
The following create_parent() calls already create 9 monthly partitions: 4 months before the current month, the current month, and 4 months ahead. If any data falls into this time range, the call will fail. Migrating the data from the default partition is out of the scope of this documentation.

SELECT partman.create_parent(
    p_parent_table      => 'public.iap_app_event',
    p_control           => 'timestamp_created',
    p_interval          => '1 month',
    p_premake           => 4,
    p_default_table     => false
);

SELECT partman.create_parent(
    p_parent_table      => 'public.iap_app_fingerprint_history',
    p_control           => 'timestamp_created',
    p_interval          => '1 month',
    p_premake           => 4,
    p_default_table     => false
);

SELECT partman.create_parent(
    p_parent_table      => 'public.iap_app_flag_history',
    p_control           => 'timestamp_created',
    p_interval          => '1 month',
    p_premake           => 4,
    p_default_table     => false
);

Indexes and constraints defined on the parent table are automatically inherited by every partition pg_partman creates, so no per-partition maintenance is required for schema changes going forward. Privileges are not inherited by the partitions but this is by design as PostgreSQL checks privileges against the parent table.

Configure a retention policy per table if old data should eventually be dropped (or detached for archival) instead of growing indefinitely. For example, to retain 24 months of data and drop older partitions automatically:

UPDATE partman.part_config
SET retention              = '24 months',
    retention_keep_table   = false
WHERE parent_table IN (
    'public.iap_app_event',
    'public.iap_app_fingerprint_history',
    'public.iap_app_flag_history'
);

Set retention_keep_table = true instead if the aged-out partitions should be detached and kept rather than dropped. This alone leaves the detached table in place in the same schema; set retention_schema to a dedicated archive schema to actually move it out for archival to cold storage. Set retention_keep_index = false to also drop the detached table’s indexes (kept by default) if it is no longer going to be queried directly.

Schedule Maintenance

pg_partman only creates the SQL functions and partition-set configuration — it does not create new partitions or apply retention on its own. A scheduled job must call partman.run_maintenance_proc() regularly. Use one of the following, in order of preference:

  1. pg_partman_bgw background worker (preferred for production, runs inside PostgreSQL, no external dependency). Add the following to postgresql.conf (locate it via SHOW config_file;, or the equivalent configuration mechanism on managed services) and restart PostgreSQL:

    shared_preload_libraries = 'pg_partman_bgw'
    pg_partman_bgw.interval = 3600
    pg_partman_bgw.role = 'partman_maintenance'
    pg_partman_bgw.dbname = 'iap'
    

    Check whether your managed PostgreSQL provider exposes pg_partman_bgw via a parameter group before relying on it; if it is not supported, use pg_cron instead.

  2. pg_cron (use when pg_partman_bgw is unavailable, e.g. on managed services that do not support it). Before it can be created, install it the same way as pg_partman (PGDG postgresql-<version>-cron package, or your cloud provider’s equivalent), then load it via shared_preload_libraries and restart the cluster (on managed services, this is typically done through a parameter group or extension allow-list):

    CREATE EXTENSION IF NOT EXISTS pg_cron;
    SELECT cron.schedule_in_database('partman-maintenance', '0 * * * *', $$CALL partman.run_maintenance_proc()$$, 'iap');
    

Note The role used to run maintenance (either pg_partman_bgw.role or the pg_cron job owner) must own, or have sufficient privileges on, the partitioned tables and the partman schema.

Verify the Setup
-- Confirm the partition sets are registered as expected; maintenance_last_run indicates whether the
-- scheduled job (pg_partman_bgw or pg_cron) is actually running
SELECT parent_table, partition_interval, premake, retention, retention_keep_table, maintenance_last_run
FROM partman.part_config;

-- List the partitions created for a given table
SELECT inhrelid::regclass AS partition
FROM pg_inherits
WHERE inhparent = 'public.iap_app_event'::regclass;

Monitor for Missing Partitions
If the scheduled maintenance job stops running (e.g. pg_cron job failure, pg_partman_bgw misconfiguration), rows for future months silently fall back into the _default partition instead of failing. A growing default partition defeats partition benefits and can degrade query performance over time. Alert on job failures and periodically verify that partman.part_config shows partitions covering the near future.

Redis Setup

Download and install the current version of Redis:

and start the Redis Server.

By default the components connect to a single (standalone) Redis instance via INAPPPROTECTION_REDIS_HOST / INAPPPROTECTION_REDIS_PORT, which is also the correct topology for managed Redis using the Enterprise cluster policy. To run against a Redis OSS cluster instead, activate the redis-cluster Spring profile (SPRING_PROFILES_ACTIVE=redis-cluster) and set INAPPPROTECTION_REDIS_CLUSTER_NODES. See Configuration Properties for the full list of Redis variables, authentication/TLS options, and cluster tuning.

Pull the Docker Images

To deploy the Docker image, you need to log in to our Artifactory repository and pull the Docker images:

docker login wultra.jfrog.io
docker pull wultra.jfrog.io/wultra-docker/iap-init:${VERSION}
docker pull wultra.jfrog.io/wultra-docker/iap-device-api:${VERSION}
docker pull wultra.jfrog.io/wultra-docker/iap-integration-api:${VERSION}
docker pull wultra.jfrog.io/wultra-docker/iap-console-api:${VERSION}
docker pull wultra.jfrog.io/wultra-docker/iap-console-web:${VERSION}

Database Initialization

The iap-init image is responsible for initializing the database schema using Liquibase. Before running this container, set the following environment variables:

DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/iap
DATASOURCE_USERNAME=$USERNAME$
DATASOURCE_PASSWORD=$PASSWORD$

Note that the iap-init container is intended for one-time execution only. It performs database initialization and then exits. It does not need to run continuously.

Running Backend Services

Once the database has been successfully initialized, start the backend services. Each component is configured via environment variables passed to the container at startup. The backend components iap-device-api, iap-console-api and iap-integration-api share a common base configuration.

Use the Correct Database URL
Backend components use Reactive Relational Database Connectivity (R2DBC). Provide a connection string with the r2dbc: prefix. When running locally, note that localhost inside a Docker container refers to the container itself, not the host machine. Use host.docker.internal to reach services running on the host.

Device API

The Device API requires the following environment variables:

DATASOURCE_URL=r2dbc:postgresql://host.docker.internal:5432/iap
DATASOURCE_USERNAME=$USERNAME$
DATASOURCE_PASSWORD=$PASSWORD$
INAPPPROTECTION_REDIS_HOST=host.docker.internal
INAPPPROTECTION_REDIS_PORT=6379

See Configuration Properties for the full list of available configuration properties for the component.

Integration API

The Integration API requires the following environment variables:

DATASOURCE_URL=r2dbc:postgresql://host.docker.internal:5432/iap
DATASOURCE_USERNAME=$USERNAME$
DATASOURCE_PASSWORD=$PASSWORD$
INAPPPROTECTION_REDIS_HOST=host.docker.internal
INAPPPROTECTION_REDIS_PORT=6379
INAPPPROTECTION_CONSOLEURL=https://<public-url-of-console-web>

INAPPPROTECTION_CONSOLEURL must point to the publicly accessible URL of the Console Web frontend. It is used to build the password reset link included in emails sent to users.

The Integration API also handles email delivery for the password reset flow. Email delivery is enabled by default and requires Gmail API credentials to be configured. Set INAPPPROTECTION_EMAIL_ENABLED=false to disable it. See Configuration Properties for the full list of email properties.

See Configuration Properties for the full list of available configuration properties for the component.

Console API

The Console API requires the following environment variables:

DATASOURCE_URL=r2dbc:postgresql://host.docker.internal:5432/iap
DATASOURCE_USERNAME=$USERNAME$
DATASOURCE_PASSWORD=$PASSWORD$
INAPPPROTECTION_REDIS_HOST=host.docker.internal
INAPPPROTECTION_REDIS_PORT=6379
INAPPPROTECTION_CONSOLEURL=https://<public-url-of-console-web>
INAPPPROTECTION_CSRFCOOKIEDOMAIN=<your-domain>

INAPPPROTECTION_CONSOLEURL must be set to the publicly accessible URL of the Console Web frontend. INAPPPROTECTION_CSRFCOOKIEDOMAIN sets the Domain attribute of the CSRF cookie. The cookie is always created with the Secure=true flag. Ensure that the CORS configuration is set accordingly to allow requests from the Console Web.

INAPPPROTECTION_ARTIFACTORYURL is optional and defaults to https://wultra.jfrog.io. Override it if SDK artifacts are hosted on a private Artifactory instance.

See Configuration Properties for the full list of available configuration properties for the component.

Finally, start the iap-device-api:${VERSION}, iap-console-api:${VERSION}, iap-integration-api:${VERSION} images.

Running Web Console

The iap-console-web container requires the following environment variable:

API_URL=https://<console-api-host>:<port>

API_URL must point to the running iap-console-api instance.

Start the iap-console-web container. Once all services are running, continue to the Configuration guide.

RASP Reporting Setup

RASP reporting collects security events and flag changes from the database and ships them to Elasticsearch via Logstash. The pipeline runs three parallel polling jobs: rasp-events, rasp-flags-set, and rasp-flags-cleared.

Download the JDBC Driver

The Logstash JDBC input plugin requires the PostgreSQL JDBC driver. It is not bundled in the Docker image and must be downloaded separately.

curl -O https://jdbc.postgresql.org/download/postgresql-42.7.5.jar

Place the downloaded JAR in the logstash/drivers/ directory of the repository.

Create a Read-Only Database User

Create a dedicated PostgreSQL user for Logstash with read-only access to the reporting tables.

Tip: You can generate a strong password locally on your computer using openssl rand -base64 12.

CREATE USER logstash WITH PASSWORD '$PASSWORD$';
GRANT CONNECT ON DATABASE iap TO logstash;
GRANT USAGE ON SCHEMA public TO logstash;
GRANT SELECT ON
    iap_app_event,
    iap_app_event_type,
    iap_app_flag,
    iap_app_flag_history,
    iap_app_flag_type,
    iap_app,
    iap_app_type,
    iap_app_device_detail
TO logstash;

Apply Elasticsearch Index Templates

Run the setup script once against your Elasticsearch instance to create the required ILM policy and index templates. The script is idempotent and safe to re-run after any template changes.

ES_URL=https://<elasticsearch-host> \
ES_USERNAME=<user> \
ES_PASSWORD=<password> \
./elasticsearch/setup.sh

The script creates:

Resource Type Description
iap-rasp-policy ILM policy Data lifecycle: hot → warm (read-only) after 90 days → delete after 365 days
iap-rasp-events Index template Mappings for security events, pattern iap-rasp-events-*
iap-rasp-flags Index template Mappings for flag changes, pattern iap-rasp-flags-*

For Azure (SaaS) deployments, use the Azure Elasticsearch endpoint URL and credentials. No other changes to the script are required.

Run Logstash

Run Logstash as a Docker container alongside the other services. Mount the pipeline configuration and JDBC driver from the repository, and provide connection details as environment variables.

Persistent Volume Required
The /usr/share/logstash/data directory must be backed by a named Docker volume. It stores the last processed row ID for each pipeline. If this volume is lost, Logstash will reprocess all historical data from the beginning.

docker run -d \
  --name iap-logstash \
  --restart unless-stopped \
  -v /path/to/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml:ro \
  -v /path/to/logstash/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro \
  -v /path/to/logstash/pipeline:/usr/share/logstash/pipeline:ro \
  -v /path/to/logstash/drivers:/usr/share/logstash/drivers:ro \
  -v logstash-data:/usr/share/logstash/data \
  -e JDBC_URL="jdbc:postgresql://<db-host>:5432/iap" \
  -e JDBC_USERNAME="logstash" \
  -e JDBC_PASSWORD="<password>" \
  -e JDBC_DRIVER_PATH="/usr/share/logstash/drivers/postgresql.jar" \
  -e ES_URL="https://<elasticsearch-host>" \
  -e ES_USERNAME="<user>" \
  -e ES_PASSWORD="<password>" \
  docker.elastic.co/logstash/logstash:8.17.4

For Azure (SaaS) deployments, replace the Elasticsearch connection variables with the Azure Elasticsearch endpoint and credentials. No other changes are required.

See Configuration Properties for the full list of available configuration properties.

Verify the Setup

# Check that all three pipelines started without errors
docker logs iap-logstash | grep -E "(ERROR|Pipeline started)"

# Verify that reporting indices exist in Elasticsearch
curl -u <user>:<password> "https://<elasticsearch-host>/_cat/indices/iap-rasp-*?v"

After the first polling interval, you should see iap-rasp-events-YYYY.MM and iap-rasp-flags-YYYY.MM indices with a growing document count.

Resilience and Data Safety

The pipeline is designed to be resilient to transient failures:

Persistent queue — Logstash is configured with queue.type: persisted. Events are written to disk before the pipeline processes them. If Logstash restarts while Elasticsearch is temporarily unavailable, no data is lost — events are replayed from the persistent queue on the next startup.

Idempotent writes — each event is written to Elasticsearch using a stable document_id derived from the PostgreSQL row ID. Reprocessing the same rows (e.g. after a metadata reset) produces no duplicates in Elasticsearch.

Source data is never consumed — unlike a message queue, the source data in PostgreSQL is not deleted by Logstash. If the metadata file is lost or reset, Logstash replays from the beginning. The idempotent writes ensure Elasticsearch remains consistent.

Recovering from a gap — if the metadata file is corrupted or points too far ahead, reset it manually:

docker exec iap-logstash sh -c 'echo "--- 0" > /usr/share/logstash/data/rasp-events.metadata'
docker exec iap-logstash sh -c 'echo "--- 0" > /usr/share/logstash/data/rasp-flags-set.metadata'
docker exec iap-logstash sh -c 'echo "--- 0" > /usr/share/logstash/data/rasp-flags-cleared.metadata'

Restart Logstash after the reset. The full history will be reprocessed and written to Elasticsearch idempotently.

For guidance on operating Logstash during extended outages, multi-instance HA deployment options, and step-by-step operational runbooks, see Logstash HA & Runbooks.

Last updated on Aug 06, 2026 (07:58) Edit on Github Send Feedback

develop

In-App Protection