Wednesday, October 28, 2026

Fixing Broken Blogger Telemetry: Upgrading to GA4, Cross-Domain Linking, and Excluding Admin Visits

If you've been running a legacy technical blog on Google Blogger, chances are your traffic telemetry is either dead or wildly distorted. Older Blogger templates relied on Universal Analytics (UA-XXXXX-Y), which stopped recording hits when Google permanently sunsetted the platform. Additionally, Blogger’s native "Stats" tab counts every scraping bot, RSS scanner, and search crawler as a legitimate human visit.

Here is an end-to-end guide on migrating your Blogger instance to modern Google Analytics 4 (GA4), configuring multi-domain tracking if you run a separate business or portfolio site, filtering out your own admin pageviews, and injecting clean, XML-safe templates.


1. Provisioning a Modern GA4 Measurement ID

GA4 properties identify websites via a Measurement ID starting with a G- prefix instead of the obsolete UA- tags.

  1. Navigate to Google Analytics.
  2. In the bottom-left corner, click Admin (gear icon).
  3. Under Data collection and modification, select Data streams.
  4. Click into your Web stream. In the top-right corner of the Web stream details pane, locate and copy your Measurement ID (e.g., G-DN6TEYJ52G).

2. Deploying the Tag to the Blogger Dashboard

Blogger features a native integration field that injects the required gtag.js headers across your entire site structure without manually modifying XML headers:

  1. Open your Blogger Dashboard.
  2. From the left-hand navigation rail, click Settings → Basic.
  3. Locate Google Analytics Measurement ID.
  4. Clear out any expired UA- code, paste your new G-XXXXXXXXXX tag, and click Save.

3. Configuring Cross-Domain Measurement

If your blog serves as a funnel to your primary company website, agency, or portfolio, sending users from your blog to your main domain typically splits the session into two disconnected visits. Cross-domain measurement keeps attribution intact:

  1. In GA4, return to Data streams → click your web stream → click Configure tag settings.
  2. Under the Settings card, click Configure your domains.
  3. Click Add condition:
    • Match type: Contains
    • Domain: Your primary site domain (e.g., yourcompany.com)
  4. Click Add condition again:
    • Match type: Contains
    • Domain: Your blog domain (e.g., yourblog.com)
  5. Click Save. GA4 will now automatically append linking query parameters across cross-domain anchor tags, preserving initial session origin, referrer sources, and conversion funnels.

4. Eliminating Self-Traffic & Bringing Filters Out of Testing

Frequent editing and testing will corrupt your metrics if your internal views aren't excluded. You should drop self-traffic at both the GA4 level and Blogger level:

A. GA4 Internal IP Exclusion
  1. In GA4 Admin, navigate to Data streams → Configure tag settings → Show all → Define internal traffic.
  2. Click Create, assign a name (e.g., Office IP), set Match type to IP address equals, and enter your public WAN IP.
Crucial Step: Taking Filters Out of Testing Mode

By default, GA4 creates the Internal Traffic data filter in Testing mode. In this mode, GA4 evaluates the traffic and flags it with a dimension, but does not actually exclude it from your metrics. You must manually activate it:

  1. Return to the Admin panel under Data collection and modification → Data filters.
  2. Click the Internal Traffic row.
  3. Scroll down to Filter state, select Active, and click Save.
  4. Confirm by clicking Activate filter. Only now will your internal visits be permanently filtered from live analytics data.
B. Blogger Dashboard Native Blocker

In the Blogger left sidebar, click Stats → scroll to the very bottom → click Manage tracking your own pageviews → check "Don't track my views for this blog". This drops a client-side tracking cookie in your browser to stop Blogger's internal counter from registering your session.

5. XML-Safe In-Article Conversion Cards & Obfuscation

When adding conversion elements or lead capture boxes under individual blog posts, standard HTML can cause compilation errors inside Blogger's XML parser. Common pitfalls include:

  • Reserved Entities: Characters like right arrows (→) will crash the XML compiler with a SAXParseException. Always use numeric entities (→) or raw UTF-8 characters (→).
  • CDATA Block Wrapping: Any JavaScript containing logical operators (like && or <) must be encapsulated within //<![CDATA[ ... //]]> blocks.
  • Scraper Obfuscation: Avoid hardcoding raw phone numbers or email addresses in post bodies. Storing contact points in Base64 and unmasking them via DOM replacement on user interaction shields you from scraping crawlers:
<!-- Lightweight Obfuscated Reveal Component -->
<span style="font-size: 13px;">Direct Support: 
  <strong id="lead-ph" style="cursor: pointer; color: #0284c7; text-decoration: underline;" 
          onclick="revLead(this)" onmouseover="revLead(this)">
    Reveal Contact Line
  </strong>
</span>

<script type="text/javascript">
//<![CDATA[
function revLead(el) {
  if (!el || el.getAttribute('data-unmasked') === '1') return;
  try {
    // Encoded '555-555-0199' in Base64
    var p = atob('NTU1LTU1NS0wMTk5');
    var a = document.createElement('a');
    a.href = 'tel:' + p.replace(/-/g, '');
    a.textContent = p;
    a.style.color = '#0284c7';
    a.style.fontWeight = '700';
    a.style.textDecoration = 'none';
    el.setAttribute('data-unmasked', '1');
    el.parentNode.replaceChild(a, el);
  } catch(e) {}
}
//]]>
</script>

6. Testing Realtime Telemetry

Once configured, verify transmission:

  1. Open a clean Incognito or Private browsing window (ensuring it's outside your filtered IP or on a mobile carrier connection).
  2. Load an article on your blog.
  3. In your Google Analytics panel, select Reports → Realtime.
  4. Within 30 seconds, you will see a live session record in the geographic distribution map alongside automatic telemetry for user engagement, scroll depth, and outbound link clicks.

Tuesday, October 27, 2026

Defending osTicket and WordPress: Incident Response and Automated Blocking with CrowdSec

If you run internet-facing web applications like osTicket and WordPress, automated credential-stuffing bots will eventually find your login endpoints. To stop bursts of "Excessive login attempts" alerts hitting a login portal (/scp/login.php), investigate the server logs, confirm zero breaches occurred, and implement a multi-layered defense using UFW and CrowdSec.

Here is the full incident breakdown and the exact steps to implement this automated security setup on an Ubuntu Linux server.


1. Immediate Mitigation via Linux Firewall

When dealing with an active brute-force flood from a persistent IP address, the fastest remediation is to drop all incoming packets at the firewall before the web server expends resources processing the requests:

sudo ufw insert 1 deny from 66.232.121.113 to any
sudo ufw reload

Why insert 1 matters: Firewalls evaluate rules sequentially from top to bottom and exit on the first match. Using insert 1 places the rule at the very top of the rule chain, ensuring the traffic is dropped before any broader allow 80/tcp or allow 443/tcp web rules can accept it.


2. Post-Incident Forensics: Confirming Zero Breach

Before moving forward, verify whether the attacking IP has compromised the system by inspecting the Apache access logs and system authentication logs:

sudo grep -rn '66.232.121.113' /var/log/apache2/
sudo grep '66.232.121.113' /var/log/auth.log

The logs can confirm the following:

  • Repeated Login Loops: Every single request from the IP was exclusively cycling between GET /scp/login.php and POST /scp/login.php.
  • No Dashboard Traversal: If an authentication attempt had succeeded, osTicket would have issued a redirect to an internal page (such as /scp/index.php or /scp/tickets.php) followed by static resource requests. No internal endpoints were ever requested.
  • No Shell Access: /var/log/auth.log showed zero inbound SSH connections, confirming no external access to the underlying OS.

3. Long-Term Automated Defense with CrowdSec

Manually blocking individual IP addresses does not scale. To automate threat mitigation across osTicket, WordPress, and SSH, we installed CrowdSec along with its iptables firewall remediation bouncer.

Step A: Install CrowdSec & the Firewall Bouncer

Add the official repository and install the engine and bouncer:

# 1. Add repository and install security engine
curl -s https://install.crowdsec.net | sudo sh
sudo apt update
sudo apt install -y crowdsec

# 2. Install the iptables/firewall remediation component
sudo apt install -y crowdsec-firewall-bouncer-iptables

Step B: Enable WordPress Threat Scenarios

To guard against WordPress user enumeration, credential attacks on wp-login.php, and wp-config.php probes, install the official collection:

sudo cscli collections install crowdsecurity/wordpress
sudo systemctl reload crowdsec

Step C: Create a Custom osTicket Brute-Force Scenario

While CrowdSec includes generic HTTP protection rules, osTicket's login behavior is best served by a dedicated scenario that monitors repeated rapid POST requests targeting /login.php.

Create /etc/crowdsec/scenarios/osticket-bf.yaml:

sudo tee /etc/crowdsec/scenarios/osticket-bf.yaml > /dev/null << 'EOF'
type: leaky
name: local/osticket-bf
description: "Detect brute-force attempts on osTicket agent and client logins"
filter: "evt.Meta.log_type in ['apache2_access', 'http_access-log'] && evt.Parsed.request contains '/login.php' && evt.Parsed.verb == 'POST'"
groupby: "evt.Meta.source_ip"
capacity: 5
leakspeed: "10s"
blackhole: "5m"
labels:
  service: http
  type: bruteforce
  remediation: true
EOF

Reload the agent and verify the new scenario is registered:

sudo systemctl reload crowdsec
sudo cscli scenarios list | grep osticket

4. Operational Health & Management Commands

With this pipeline active, CrowdSec automatically monitors Apache access logs, parses incoming events, and instructs the firewall bouncer to drop malicious traffic dynamically. Useful operational commands include:

  • Check Live Parser & Bouncer Metrics: sudo cscli metrics
  • View Active Bans: sudo cscli decisions list
  • View Triggered Alerts: sudo cscli alerts list
  • Manually Ban an IP: sudo cscli decisions add --ip <IP> --duration 720h --reason "brute force"
  • Remove a False Positive Ban: sudo cscli decisions delete --ip <IP>
cfb3339690c974a7

Wednesday, October 21, 2026

Patching the Engine: Remediating Critical PHP Flaws in Production Docker Stacks

Security Advisory & Incident Response

Remediating Critical PHP Engine Flaws in Production Docker Stacks

A step-by-step technical post-mortem: runtime verification, upstream tag validation, and container cache resolution.

1. Incident Context: The Threat Vector

On October 21, 2026, coordinated emergency advisories dropped across the web infrastructure community addressing critical unauthenticated Remote Code Execution (RCE) flaws deep within core PHP request parsing and memory management routines (CVE-2026-109618 and CVE-2026-109619). The vulnerabilities reside at the engine layer, meaning typical web application firewalls and reverse proxies forwarding raw HTTP traffic cannot reliably inspect or sanitize malicious triggers.

Standard security targets required immediate updates to safe patch levels across all active branches:

  • PHP 8.2: Upgraded to 8.2.35+
  • PHP 8.3: Upgraded to 8.3.36+
  • PHP 8.4: Upgraded to 8.4.27+

2. Reverse Proxies & Ingress Filtering

A frequent initial assumption is that placing workloads behind an edge reverse proxy (such as Traefik, Nginx, or HAProxy) provides inherent layer-7 protection. In practice, unless the proxy actively implements a deep-packet inspection WAF (like Coraza/ModSecurity) with explicit signatures, it merely terminates TLS and forwards standard payloads straight downstream to the vulnerable PHP worker pools.

During a zero-day disclosure window, edge proxies should primarily be leveraged as gatekeepers—either enforcing immediate IP allowlisting or temporarily terminating public routes while downstream container images rebuild upstream.

3. Execution Workflow: Upgrading Docker Runtimes

Below is the operational runbook executed to audit, update, and harden a production container stack without relying on blind image pull assumptions.

Step 3.1: Identifying Running Runtimes

Never rely strictly on host labels or compose filenames. Execute directly against the containerized binary to verify the compiled engine version:

# Query the running container binary
docker exec -it app-core-web-production php -v

# Vulnerable baseline detected:
# PHP 8.3.25 (cli) (built: Sep  8 2025 21:19:20) (NTS)
# Zend Engine v4.3.25, OPcache v8.3.25

Step 3.2: Upstream Image Verification & Pull

Upstream registry builds lag behind official patch declarations. Pulling an image tag before automated pipelines build and push the updated binaries merely re-pulls the vulnerable layers. Checking repository tags and testing transiently ensures the fix is present before recreating the production workload:

# Pull new base layers from registry
docker pull wordpress:php8.3-apache

# Transiently spin up and confirm the underlying patch level
docker run --rm wordpress:php8.3-apache php -v

# Confirmed safe build:
# PHP 8.3.36 (cli) (built: Oct 21 2026 15:57:18) (NTS)
# Zend Engine v4.3.36, OPcache v8.3.36

Step 3.3: Configuration Hardening & Stack Recreation

To provide defense-in-depth against upload-progress heap-grooming vectors, disable progress tracking globally inside the runtime configuration (custom-php.ini):

session.upload_progress.enabled = 0

Trigger container recreation to swap the running process over to the newly verified image layer:

# Recreate production container with forced re-bind
docker compose up -d --force-recreate

# Final verification against production runtime
docker exec -it app-core-web-production php -v

4. Troubleshooting Image Cleanup: The "In-Use" Image Trap

When attempting to purge old, superseded images (often showing as <none> or legacy build hashes), Docker daemon safety checks may prevent deletion with errors stating the image is still in use.

Key Docker Behavior: Even if a container is in a Stopped state (such as staging branches, dev sandboxes, or test instances), Docker maintains an active lock on that image layer. An image cannot be pruned until every associated container is either removed or recreated against a newer image layer.

To release image references without breaking environments:

  1. For Active/Staging Workloads: Recreate the stopped or staging stacks using the freshly pulled image. As each container binds to the new image, its reference to the legacy image drops automatically.
  2. For Deprecated Environments: Remove dormant container instances explicitly:
    docker rm sandbox-app-dev staging-web-instance
    docker image prune -f

Operational Takeaways

  • Do not assume :latest tags or container rebuilds pull patched code until build timestamps and version binaries are verified inside a transient test container.
  • Layer-7 reverse proxies do not insulate backend engines from deep runtime parsing bugs without explicit WAF rules or IP-level lockdown.
  • Stopped containers maintain active image layer locks; audit staging and sandbox workloads regularly during security patch cycles.

ec6a761bc8e895a0