NPMplus + OpenAppsec

NPMplus + OpenAppsec

Finally settled on a WAF for my production and development applications

Took a considerable amount of time and experimentation, something that I don't have a lot of lately after the new year but finally catching up on a lot of things and the hypervisor rebuild expedited a great deal.

As I stated previously becoming more independent of cloud services and hosting more internally, which requires a WAF.

So here is the link, so you know what the code at the bottom of this post is for

NPMplus | open-appsec

There is some serious configuration that needs to go on to get this working

  • I recommend a cloud management deployment at this stage
  • You need to organize your stacks in your hypervisor
  • It chews CPU cycles so don't place your intensive services in the same stack like GitLab
  • You need to bring this stack up first and then attach other stacks as required to its network

So, looking for a docker compose and environment file here you go.

# Stack configuration for NPMplus with OpenAppSec and Cloudflared
# This file defines the services and their configurations for running NPMplus with OpenAppSec and Cloudflared.

# Development Notes:
# - Ensure you have the necessary environment variables set in a .env file.
# - The NPMplus service will manage SSL certificates for the specified domain.
# - OpenAppSec will provide application security features.  
# - Cloudflared will handle secure tunneling to the NPMplus service.

# THIS IS A WORKING CONFIGURATION FILE

# Updated 21-07-2025

services:
  # https://github.com/maxmind/geoipupdate
  geoipupdate:
    image: docker.io/maxmindinc/geoipupdate:latest
    container_name: npmplus-geoipupdate
    hostname: npmplus-geoipupdate
    restart: always
    environment:
      TZ: ${TZ}
      GEOIPUPDATE_EDITION_IDS: GeoLite2-Country GeoLite2-City GeoLite2-ASN
      GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
      GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
      GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY}
    volumes:
      - /srv/npmplus/goaccess/geoip:/usr/share/GeoIP
      - /srv/npmplus/geoipupdate/log:/var/lib/geoipupdate
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    security_opt:
      - no-new-privileges:true
    networks:
      - openappsec-net

  # https://docs.openappsec.io/integrations/npmplus
  openappsec-agent:
    image: ghcr.io/openappsec/agent:latest
    container_name: openappsec-agent
    hostname: openappsec-agent
    restart: always
    depends_on:
      - geoipupdate
    environment:
      AGENT_TOKEN: ${APPSEC_AGENT_TOKEN}
      USER_EMAIL: ${APPSEC_USER_EMAIL}
      AUTO_POLICY_LOAD: ${APPSEC_AUTO_POLICY_LOAD}
      LOG_LEVEL: debug
      TZ: ${TZ}
      registered_server: NPMplus
    command: /cp-nano-agent
    volumes:
      - shared-data:/data
      - /srv/npmplus/appsec-localconfig:/ext/appsec
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    security_opt:
      - no-new-privileges:true
    networks:
      - openappsec-net

  # ttps://github.com/ZoeyVid/NPMplus
  npmplus:
    image: docker.io/zoeyvid/npmplus:latest
    container_name: npmplus
    hostname: npmplus
    restart: always
    depends_on:
      - openappsec-agent
    environment:
      TZ: ${TZ}
      ACME_EMAIL: ${ACME_EMAIL}
      NGINX_HTTP_HOSTS: ${DOMAIN}
      NGINX_HTTPS_HOSTS: ${DOMAIN}
      NGINX_LOAD_OPENAPPSEC_ATTACHMENT_MODULE: true
      LOGROTATE: true
      LOGROTATIONS: 7
      GOA: true
    ports:
      # Modify these ports as needed depending on service requirements
      - 80:80
      - 443:443
      - 443:443/udp
      - 81:81
    volumes:
      - shared-data:/data
      - /srv/npmplus/localconfig:/ext/appsec
      - /srv/npmplus/nginx:/opt/npmplus/nginx
      - /srv/npmplus/logs:/opt/npmplus/logs
      - /srv/npmplus/certs:/opt/npmplus/certs
      - /srv/npmplus/goaccess:/opt/npmplus/goaccess
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    security_opt:
      - no-new-privileges:true
    networks:
      - openappsec-net

networks:
  openappsec-net:
    driver: bridge
    name: openappsec-net
    attachable: true
    driver_opts:
      com.docker.network.bridge.default_bridge: "false"
      com.docker.network.bridge.enable_icc: "true"


volumes:
  shared-data:

Make sure you adjust the volumes as you require (I like putting them in the srv directory as that is what the damn thing is for)

In Linux, the /srv directory—short for “service”—is intended to hold data for services provided by the system. Think of it as a home for server-specific content, particularly when the machine is hosting things like web, FTP, or other network services.

As you can see, we need an environment file. Since I deploy via Portainer I have not referenced it in the docker compose file, but it is below

# ─────────────────────────────────────────────────────
# REQUIRED
# ─────────────────────────────────────────────────────

# Timezone for all containers
TZ=Australia/Perth

# Your open-appsec SaaS token
APPSEC_AGENT_TOKEN=[get this from the openappsec management portal]

# Domain served by NPMplus
DOMAIN=[your TLD]

# ACME (Let's Encrypt) email
ACME_EMAIL=[your registered email address]

# MAXMIND account information
GEOIPUPDATE_ACCOUNT_ID=[your ID number]
GEOIPUPDATE_LICENSE_KEY=[licence key - one for free]
GEOIPUPDATE_FREQUENCY=24

# The below is not used at this time - this code has been removed
# Cloudflare Tunnel
CF_TUNNEL_TOKEN=[your tunnel token ID]
CF_TUNNEL_NAME=[your tunnel name]

# ─────────────────────────────────────────────────────
# OPTIONAL
# ─────────────────────────────────────────────────────

# Email to register in open-appsec UI
APPSEC_USER_EMAIL=[your openappsec registered email address]

# Auto-reload local_policy.yaml from ./appsec-localconfig
APPSEC_AUTO_POLICY_LOAD=true

Don't forget to attach the frontend of other stacks to the network designated in the docker compose file so something like this.

# Extract of the ghost stack
# Not a complete so it wont work by itself and is supplied as an example
 
ghost:
    # Do not alter this without updating the Tinybird Sync container as well
    image: ghost:${GHOST_VERSION:-5-alpine}
    container_name: ghost
    hostname: ghost
    restart: always
    labels:
      - "com.docker.compose.project=ghost"
      - "com.docker.compose.service=ghost"
      - "com.centurylinklabs.watchtower.enable=false"
      - "dockflare.enable=false" 
      - "dockflare.hostname=ghost.${DOMAIN}"
      - "dockflare.service=http://ghost:${GHOST_PORT}" 
      - "dockflare.no_tls_verify=true"
      - "dockflare.originsrvname=ghost.${DOMAIN}" 
      - "dockflare.originsrvport=${GHOST_PORT}"
      - "dockflare.originsrvproto=http" 
    # Expose the port externally
    ports:
      - ${GHOST_PORT}:2368
    environment:
      NODE_ENV: production
      url: http://ghost.${DOMAIN}
      # Database configuration
      database__client: mysql
      database__connection__host: db
      database__connection__user: ${DATABASE_USER}
      database__connection__password: ${DATABASE_PASSWORD}
      database__connection__database: ghost
      # Miscellaneous settings
      enableDeveloperExperiments: ${ENABLE_DEVELOPER_EXPERIMENTS}
      labs__ActivityPub: ${ENABLE_ACTIVITYPUB}
      labs__trafficAnalytics: ${ENABLE_TRAFFIC_ANALYTICS}
      # Mail configuration
      mail__transport: ${MAIL_PROTOCOL}
      mail__from: ${MAIL_ADDRESS}
      mail__options__service: ${MAIL_SERVICE}
      mail__options__host: ${MAIL_HOST}
      mail__options__port: ${MAIL_PORT}
      mail__options__secureConnection: ${MAIL_SECURITY}
      mail__options__secure: ${MAIL_SECURITY}
      mail__options__auth__user: ${MAIL_USER}
      mail__options__auth__pass: ${MAIL_PASS}
      # Tinybird configuration - not used in this stack
      tinybird__tracker__endpoint: https://${DOMAIN}/.ghost/analytics/api/v1/page_hit
      tinybird__adminToken: ${TINYBIRD_ADMIN_TOKEN}
      tinybird__workspaceId: ${TINYBIRD_WORKSPACE_ID}
      tinybird__tracker__datasource: analytics_events
      tinybird__stats__endpoint: ${TINYBIRD_API_URL:-https://api.tinybird.co}
    volumes:
      - /srv/ghost/content:/var/lib/ghost/content:rw
      - /srv/ghost/logs:/var/lib/ghost/logs:rw
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro
    depends_on:
      db:
        condition: service_healthy
      activitypub:
        condition: service_started
        required: false
    healthcheck:
      test: ["CMD", "nc", "-z", "localhost", "2368"]
      interval: 10s 
      timeout: 5s
      retries: 5
    cap_add:
      - SYS_TIME
    security_opt:
      - no-new-privileges:true
    networks:
      - ghost-net
      - dockerflare-net

This works and will get you going. Note there is considerable configuration to be done afterwards and the password to get into the NPMplus management interface will be in the docker logs for that service.

Login straight away and reset your admin credentials or it will be a rm -rf command, after you delete the stack and try again.

#enoughsaid