MAX98357A for Radza Zero 3 – Giving Skelly a Voice with I2S and GPIO

When I started my AI Powered Skelly project built with a Radxa Zero 3 I quickly discover that the board does not ship with on‑board audio. This was a problem, because I really wanted to utilize the existing speaker within the 3ft Dancing Skeleton animatronic as a more natural source of audio output. To solve this I choose to use a MAX98357A mono amplifier to drive audio output to small speaker. This article walks you through everything you need to get the MAX98357A working on a Radxa Zero 3, from wiring the pins to enabling I2S software and finally creating the custom device‑tree overlay that makes the kernel aware of the hardware.

 MAX98357A

Why Choose MAX98357A for Radxa Zero 3?

FeatureMAX98357ATypical Alternatives
Power consumption< 100 mW (idle)> 200 mW for many DACs
Output typeMono, 3 W Class‑DStereo, often larger footprint
InterfaceI2S digital audio input onlyPCM/I2C/USB combos
Gain controlFixed (9 dB default) or external pinVariable via software registers
Shutdown pinOptional hardware lineOften not present

Because the amplifier needs only three I2S signals (BCLK, LRCK and DATA) plus power and ground, it can be wired directly to the standard I2S pins on the Radxa Zero 3 without any extra level shifters. The board’s 5 V rail supplies the amplifier’s VIN, while the GND pins provide a clean reference.

GPIO Pinout – Mapping MAX98357A to Radxa Zero 3

The Radxa Zero 3 exposes its I2S‑3 controller (named I2S3_M0) on the following header pins:

MAX98357A pinFunctionRadxa Zero 3 Pin #SoC GPIO name
VIN5 V power2 or 4 5v VDD_5V
GNDGround6, 9, 14, 20, 25, 30, 34, 39GND
BCLKI2S Bit Clock12I2S3_SCLK_M0
DIN (DATA)I2S Serial Data In40I2S3_SDO_M0
LRC (LRCK)I2S Left‑Right Clock35I2S3_LRCK_M0
GAIN (optional)Fixed gain selector – connect to GND for 6 dB, leave floating for default 9 dBany free GND (e.g., 20)GND
SD (Shutdown)Active‑low shutdown – pull low to muteany free GPIO (e.g., 16 with optional PWM)GPIO3_B1/PWM8_M0

Tip: The GAIN and SD pins are optional. For a simple “always on” configuration you can leave them floating. If you need hardware mute, connect the SD pin to a spare GPIO and drive it low when you want silence.

Wiring Diagram

Wiring MAX98357A for Radxa Zero 3
Radxa Zero 3                 MAX98357A
---------------------------  -----------------
Pin 4   (+5 V)               VIN  (Red)
Pins 6/9/... (GND)           GND  (Black)
Pin 12  I2S3_SCLK_M0 (BCLK)  BCLK (Blue)
Pin 40  I2S3_SDO_M0  (DATA)  DIN  (Orange)
Pin 35  I2S3_LRCK_M0 (LRCK)  LRC  (Green)
Pin 16  (optional)           GAIN (Purple Doted)
Pin 18 (optional)            SD   (Black Doted)

All connections are 5 V tolerant on the Radxa Zero 3, and the MAX98357A operates comfortably from 2.7 V up to 5.5 V, so no level shifting is required.

Preparing the Software – Enabling I2S in the Kernel

The Radxa Zero 3 runs a Debian‑based OS (or Ubuntu) with a Linux kernel that already contains an I2S controller driver (i2s-axg). However, the default device tree does not expose the I2S3_M0 peripheral for audio output. To make it usable you must:

  1. Install the overlay tooling – Radxa provides rsetup and a set of scripts to compile and install custom overlays.
   sudo apt-get update
   sudo apt-get install rsetup device-tree-compiler
  1. Disable any conflicting overlay – The board ships with an i2s3-m0 overlay that is intended for microphone input. You need to remove or disable it before loading your speaker overlay.
   sudo rsetup overlay disable i2s3-m0
  1. Compile the custom overlay (shown in the next section) and install it:
   sudo rsetup overlay add radxa-zero3-max98357a.dts
  1. Reboot to let the kernel load the new device‑tree node.
   sudo reboot
  1. Verify that the I2S PCM device appears under /dev.
   aplay -l
   # You should see something like:
   # card 0: Rockchip [rockchip i2s], device 0: I2S3 PCM [...]
  1. Test audio playback – Use aplay with a raw PCM file or any WAV that matches the default format (16‑bit, 44.1 kHz, mono).
   aplay -D plughw:0,0 /usr/share/sounds/alsa/Front_Center.wav

If you hear sound from your speaker, the hardware and software stack are correctly configured.

The Custom Device‑Tree Overlay – radxa-zero3-max98357a.dts

Below is the complete overlay source that tells the kernel to expose I2S3_M0 as a PCM output and optionally controls the GAIN and SD pins as GPIOs.

/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>

/ {
    metadata {
        title = "Enable MAX97357A on I2S3-M0";
        compatible = "radxa,zero3";
        category = "audio";
        description = "Enable MAX97357A on I2S3-M0";
        exclusive = "GPIO3_A0", "GPIO3_A3", "GPIO3_A4", "GPIO3_A5", "GPIO3_A6", "i2s3_2ch";
    };
};

&{/} {
    max98357a_codec: max98357a {
        #sound-dai-cells = <0>;
        compatible = "maxim,max98357a";
        sdmode-gpios = <&gpio3 RK_PA0 GPIO_ACTIVE_HIGH>;
        sdmode-delay = <5>;
        status = "okay";
    };

    sound_ext_card: sound-ext-card {
        #address-cells = <1>;
        #size-cells = <0>;
        status = "okay";
        compatible = "simple-audio-card";
        simple-audio-card,format = "i2s";
        simple-audio-card,mclk-fs = <256>;
        simple-audio-card,name = "snd_max98357a_dac";
        simple-audio-card,dai-link@0 {
            reg = <0>;
            format = "i2s";
            cpu {
                sound-dai = <&i2s3_2ch>;
            };
            codec {
                sound-dai = <&max98357a_codec>;
            };
        };
    };
};

&i2s3_2ch {
    pinctrl-0 = <&i2s3m0_lrck &i2s3m0_sclk &i2s3m0_sdi &i2s3m0_sdo>;
    status = "okay";
};

How the Overlay Is Applied

  1. Compilersetup overlay add radxa-zero3-max98357a.dts runs dtc -@ -I dts -O dtb internally and places the resulting .dtbo file under /boot/overlays.
  2. Activate – The same command updates /boot/extlinux/extlinux.conf to load the overlay at boot.
  3. Boot – During kernel initialization, the overlay patches the base device tree, making the simple-audio-card node visible to ALSA.

Controlling Shutdown from Userspace

Because we exposed the optional pins as standard Linux GPIOs, you can manipulate them with the sysfs interface or via the newer libgpiod library.

Using sysfs (quick test)

# Export GPIO11 (shutdown) and keep high (amp on)
echo 16 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio16/direction
echo 1 > /sys/class/gpio/gpio16/value  # 1 = not shutdow

Using libgpiod (modern approach)

#include <gpiod.h>

int main(void) {
    struct gpiod_chip *chip;
    struct gpiod_line *sd;

    chip = gpiod_chip_open_by_name("gpiochip3");
    sd   = gpiod_chip_get_line(chip, 1);    // GPIO16

    gpiod_line_request_output(sd,   "max98357a_sd",   1);

    /* ... later you can toggle them */
    gpiod_line_set_value(sd,   0); // shutdown amp
}

Container‑Friendly Deployment

If you are running audio workloads inside Docker on the Radxa Zero 3, keep the following in mind:

RequirementHow to satisfy
Access to /dev/sndAdd --device /dev/snd:/dev/snd (Docker) or mount the device in the pod spec.
GPIO controlMount /sys/class/gpio read‑write or use the gpiod socket (/run/gpiod).
Real‑time scheduling (optional)Use --cap-add SYS_NICE and set ulimit -r 99.
Overlay persistenceThe overlay is applied at boot, so containers do not need to modify it. Just ensure the host kernel has loaded the PCM device before container start.

A minimal Dockerfile for an audio player might look like:

FROM debian:bookworm-slim

RUN apt-get update && \
    apt-get install -y alsa-utils libgpiod2 && \
    rm -rf /var/lib/apt/lists/*

COPY myplayer.sh /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/myplayer.sh"]

Run it with:

docker run --rm -it \
  --device /dev/snd:/dev/snd \
  -v /sys/class/gpio:/sys/class/gpio:rw \
  my-audio-player

Now your container can play back WAV files through the MAX98357A and mute/unmute using GPIO.

Troubleshooting Checklist

SymptomLikely CauseFix
No sound, aplay returns “Invalid argument”I2S node not enabledVerify overlay loaded (dmesg | grep i2s3) and run aplay -l.
Crackling or clicksClock mismatch or bad power supplyEnsure VIN is stable 5 V, add a decoupling capacitor (10 µF) near the amplifier.
Speaker only plays one channelLRC pin mis‑wired or wrong pinmuxDouble‑check that Pin 35 is connected to LRC and i2s3_m0_pins includes it.
Amplifier stays silent even after GPIO highSD pin tied lowRemove the shutdown connection or set GPIO16 value to 1.
Gain not changingGAIN pin left floating while expecting -6 dBConnect GAIN to a GPIO and drive it low, or permanently tie to GND for fixed gain.

Use journalctl -k to see kernel messages about the audio card; any “probe failed” lines usually indicate a mis‑matched overlay.

Security Considerations

Even though this guide focuses on hardware integration, running audio services on an edge device still raises security questions:

  1. GPIO Exposure – Unrestricted access to /sys/class/gpio allows any local user (or compromised container) to toggle the amplifier’s pins, potentially causing denial‑of‑service or odd behavior. Restrict GPIO permissions using Linux capabilities or mount namespaces.
  2. Audio Injection – If your device accepts network streams, validate source authenticity (TLS, signed manifests) before feeding data to ALSA.
  3. Kernel Attack Surface – Custom overlays modify the kernel’s view of hardware. Ensure only trusted administrators can write overlay files; use immutable boot partitions where possible.

Conclusion

The MAX98357A for Radxa Zero 3 offers a lightweight, low‑cost way to add mono audio output to any project that runs on this SBC. By wiring three I2S pins and enabling the I2S controller through a custom device‑tree overlay I was able to give the AI Skelly the power to speak.

Up next we look at how to programmatic trigger an event, in order to capture an image from a simple attached webcam. Then how to process the image via a standard openAI style API call to a local AI service like ollama serve or LMStuido server. In order to ask targeted questions about the capture image, such as the Halloween costumes in frame. The we can use the prompt response to generate a unique voice line response with services like piper. All while keeping everything completely offline for privacy and security.

Creating a Simple Device Trust Gateway Using Device Certificates

In the evolving world of cybersecurity, identity-based access alone is no longer sufficient. The modern Zero Trust model mandates that access decisions consider not just the user but also the device. A user might be who they claim to be, but what if they’re logging in from a compromised machine or a jailbroken phone?

That’s where a device trust gateway comes in—a simple, scalable method to enforce access controls based on both user identity and device posture. Surprisingly, this doesn’t require complex architecture. In fact, with just a few lines of configuration in common web proxies like NGINX, you can create a robust checkpoint to validate device certificates before allowing application access.

In this post, we’ll explore how to build a simple yet effective device trust gateway using web proxy configurations, why it matters, and how it enhances your Zero Trust posture.

What Is a Device Trust Gateway?

device trust gateway is a proxy layer that sits in front of applications and checks whether the connecting device presents a valid, cryptographically signed certificate. This certificate—typically issued by a corporate Certificate Authority (CA)—acts as a machine identity, verifying that the device is registered, managed, and secure.

By validating the certificate before allowing a user session to proceed, organizations can enforce stronger controls such as:

  • Allowing access only from corporate-managed endpoints
  • Blocking jailbroken or unmanaged devices
  • Issuing short-lived access tokens only after successful posture checks

This approach complements MFA and SSO. Even if credentials are phished or stolen, an attacker can’t authenticate without access to a trusted device.

How It Works

  1. Device Enrollment: Devices are provisioned with client certificates from an internal CA.
  2. Proxy Enforcement: A reverse proxy (like NGINX or Apache) is configured to validate client certificates.
  3. Access Control: Only clients presenting valid certificates can reach upstream applications or IdPs (Identity Providers).
  4. Logging and Auditing: All device certificate checks are logged for forensics and compliance.

Why This Matters

In many organizations, devices are a weak link. Remote work, BYOD, and cloud-native services increase the risk of unmanaged or misconfigured endpoints.

By enabling device trust enforcement at the proxy level, you:

  • Avoid re-architecting your identity system
  • Add a powerful security control with minimal code changes
  • Stop attackers who steal credentials but don’t have trusted hardware

The best part? You likely already have the infrastructure to make it happen.

NGINX: Enforcing Client Certificate Validation

NGINX makes it straightforward to enable cleintAuth and client certificate validation.

server {
    listen 443 ssl;
    server_name secure.mycompany.com;

    ssl_certificate /etc/nginx/certs/server.crt;
    ssl_certificate_key /etc/nginx/certs/server.key;
    ssl_client_certificate /etc/nginx/certs/ca.crt; # Your CA Chain
    ssl_verify_client on;# <‑ key line

    location / {
        proxy_pass http://internal-app;
        proxy_set_header X-Client-Cert $ssl_client_cert;
        proxy_set_header X-Client-DN  $ssl_client_s_dn;
    }
}

In this snippet:

  • ssl_client_certificate points to the CA that signed your device certificates
  • ssl_verify_client on enforces certificate presentation
  • The subject DN is passed upstream for audit or additional policy checks

If a device doesn’t present a valid certificate, NGINX terminates the connection.

Note: The client cert can be passed to through the proxy to other backend services using the nginx variable $ssl_client_cert which contains the entire URL encoded client certificate in PEM format.

Optional: Enforce Device Policies

If you want to go beyond “certificate is valid” and enforce per‑device rules, leverage OpenSSL extensions or X.509 Subject Alternative Names (SAN). For example:

# Add a custom extension in the CSR:
openssl req -new -key device-01.key.pem \
    -subj "/CN=device-01.acme.com/O=Acme Devices/C=US" \
    -addext "subjectAltName = @alt_names" \
    -config <(cat /etc/ssl/openssl.cnf <(printf "[alt_names]\nrole=admin\n"))

Then in nginx you can inspect $ssl_client_s_dn or $ssl_client_cert and use map directives to block or allow based on the role.

Apache HTTPD: A Similar ClientAuth Approach

Apache’s mod_ssl module can perform the same function.

<VirtualHost *:443>
    ServerName secure.mycompany.com

    SSLEngine on
    SSLCertificateFile /etc/httpd/certs/server.crt
    SSLCertificateKeyFile /etc/httpd/certs/server.key
    SSLCACertificateFile /etc/httpd/certs/ca.crt
    SSLVerifyClient require

    <Location />
        ProxyPass http://internal-app/
        ProxyPassReverse http://internal-app/
    </Location>
</VirtualHost>

Apache enforces client cert verification with SSLVerifyClient require, ensuring only trusted devices make it through.

Monitoring & Logging

Nginx logs each handshake, including whether client cert verification succeeded. Add a custom log format:

log_format devicelog '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     'client_cert="$ssl_client_verify" '
                     'cn="$ssl_client_s_dn"';
access_log /var/log/nginx/device_access.log devicelog;

Now you can audit which devices accessed the gateway, detect expired certs, or spot anomalies.

Testing the Gateway

Valid Device – On a client machine, install device-01.cert.pem and device-01.key.pem. Or use curl:

curl -k --cert device-01.cert.pem \
     --key  device-01.key.pem \
     https://proxy.acme.com/

You should get the backend response.

  • Invalid Device – Remove or rename the cert/key and try again; you’ll receive a 403.
  • Expired Certificate – Tamper with device-01.cert.pem’s validity period or use openssl x509 -in device-01.cert.pem -noout -dates to verify expiration. The gateway will reject it automatically.

Device Trust Gateway Flow

Device Trust Gateway Authentication work flow

Steps:

  1. Device connects to proxy and presents client certificate
  2. Proxy checks cert against trusted CA
  3. If valid, forwards request to application
  4. If invalid, terminates connection

Implementation Tips

  • Use short-lived device certificates (e.g., 24 hours)
  • Automate provisioning with MDM scripts and/or SCEP
  • Use headers like X-Client-Cert to enrich identity at the application layer
  • Monitor failed certificate handshakes as potential threats

Conclusion

  • Fast Implementation – Adding just two lines (ssl_verify_client on + ssl_client_certificate) turns any TLS‑enabled proxy into a device trust gateway.
  • Zero‑Trust Foundation – Every device must prove its identity before accessing sensitive resources.
  • Scalable – The same CAs can issue thousands of certificates; you can automate provisioning via scripts or PKI tools like step-ca.

Final Thoughts

You don’t need to overhaul your infrastructure to implement device trust. Adding a few lines of proxy configuration can provide a powerful gateway that ensures only secure, trusted devices can access your applications.

In a Zero Trust world, identity is not enough. Trust must be earned—and verified—by the devices themselves.

Kubernetes Harvester to Gather Credentials with Limited Access

Project URL: https://github.com/sleventyeleven/Kubernetes-Harvester

Kubernetes Harvester Example Run

What is Kubernetes Harvester?

Harvester is a new python based project that attempts to leverage access in order to gather potentially sensitive information. Its designed to either leverage the access of users credentials or the default access granted to a pod via automountServiceAccountToken, which I wrote about recently. The harvester.py script currently primarily targets pod container environment variables, container manifest environment variables, and config map entries utilized as environment variables, to look for potential credentials.

Why Create A Credential Harvester

The default admission controls in many of the Kubernetes implementation apply a read/view policy to newly created users. However custom policies, admissions, and operators have become more common place. What’s more troublesome is the read permissions given to the automountServiceAccountToken by default. Without adjusting or disabling service tokens, compromised containers could effectively read all pod specs in all namespaces. With access to all pod specs, an attacker could potentially gather credentials or other sensitive information. Harvester is a tool that attempts to help automate the review process.

How Kubernetes Harvester Works

The harvester.py script utilizes the automountServiceAccountToken mounted within a given container or the standard user credentials within the Kube config file (~/.kube/config). Then the Kube API server is queried to look for sensitive information within the pod spec of each pod in the following steps.

  1. Use access to request pod specs for all namespaces within the cluster.
  2. Parse all pod specs to map and dedupe container container information
  3. Review each containers environment variables for sensitive values
  4. Review each config map entry, mapped to container environment variables for sensitive values
  5. Attempt to pull each container image and review the manifest environment variables for sensitive values
  6. Attempt to request authentication tokens from the internal metadata API for each of the major cloud provider

Other Resources:

  • Introduction to Kubernetes – A Free introduction course diving into Kubernetes as a tool for containerized infrastructure. Its a a great place to begin if your just getting started with Kubernetes.
  • The Linux Foundations Official Course – This is the most robust general knowledge based course I’ve seen. If you want to learn Kubernetes and how to do almost anything with it, get the CKA + CKAD combo package.