diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c26b965..8ff76348 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -310,6 +310,76 @@ jobs: path: | ~/rpmbuild/RPMS/${{ env.ARCH }}/${{ env.AIKIDO_ARTIFACT_RELEASE }} + build_lambda_layers: + name: Build Lambda layers php-${{ matrix.php_version }} + runs-on: ubuntu-24.04 + needs: [ build_libs, build_php_extension_nts ] + strategy: + matrix: + php_version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get Aikido version + run: | + AIKIDO_VERSION=$(grep '#define PHP_AIKIDO_VERSION' lib/php-extension/include/php_aikido.h | awk -F'"' '{print $2}') + echo "AIKIDO_VERSION=$AIKIDO_VERSION" >> $GITHUB_ENV + echo "AIKIDO_LIBZEN_VERSION=0.1.60" >> $GITHUB_ENV + + - name: Download NTS extension + uses: actions/download-artifact@v4 + with: + name: aikido-extension-php-${{ matrix.php_version }}-nts-x86_64 + path: artifacts/extension + + - name: Download agent + uses: actions/download-artifact@v4 + with: + name: aikido-agent-x86_64 + path: artifacts/agent + + - name: Download request processor + uses: actions/download-artifact@v4 + with: + name: aikido-request-processor-x86_64 + path: artifacts/request-processor + + - name: Download Aikido Zen Internals Lib + run: | + curl -L -O https://github.com/AikidoSec/zen-internals/releases/download/v${{ env.AIKIDO_LIBZEN_VERSION }}/libzen_internals_x86_64-unknown-linux-gnu.so + + - name: Build Lambda layer zip + run: | + LAYER_DIR=lambda-layer + AIKIDO_DIR=$LAYER_DIR/aikido-${{ env.AIKIDO_VERSION }} + BREF_CONF_DIR=$LAYER_DIR/bref/etc/php/conf.d + + mkdir -p $AIKIDO_DIR $BREF_CONF_DIR + + find artifacts/extension -name "aikido-extension-php-${{ matrix.php_version }}-nts.so" -exec cp {} $AIKIDO_DIR/ \; + find artifacts/agent -name "aikido-agent" -exec cp {} $AIKIDO_DIR/ \; + find artifacts/request-processor -name "aikido-request-processor.so" -exec cp {} $AIKIDO_DIR/ \; + cp libzen_internals_x86_64-unknown-linux-gnu.so $AIKIDO_DIR/ + chmod +x $AIKIDO_DIR/aikido-agent + + echo "extension=/opt/aikido-${{ env.AIKIDO_VERSION }}/aikido-extension-php-${{ matrix.php_version }}-nts.so" > $BREF_CONF_DIR/ext-aikido.ini + + ls -la $AIKIDO_DIR/ + + cd $LAYER_DIR + zip -r ../aikido-firewall-bref-lambda-layer-php-${{ matrix.php_version }}-x86_64.zip . + + - name: Archive Lambda layer + uses: actions/upload-artifact@v4 + with: + name: aikido-firewall-bref-lambda-layer-php-${{ matrix.php_version }}-x86_64 + if-no-files-found: error + path: | + aikido-firewall-bref-lambda-layer-php-${{ matrix.php_version }}-x86_64.zip + build_deb: name: Build deb ${{ matrix.arch == '' && 'x86_64' || 'arm' }} runs-on: ubuntu-24.04${{ matrix.arch }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e46e649a..cc1f4260 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,10 +26,16 @@ jobs: with: pattern: | aikido-php-firewall* + + - name: Download Lambda layer artifacts + uses: actions/download-artifact@v4 + with: + pattern: | + aikido-firewall-bref-lambda-layer-* - name: List Artifacts run: | - ls -l + ls -lR pwd - name: Deploy to GitHub Release (draft) @@ -38,3 +44,4 @@ jobs: draft: true files: | ./**/aikido-php-firewall* + ./**/aikido-firewall-bref-lambda-layer-* diff --git a/lib/agent/constants/constants.go b/lib/agent/constants/constants.go index bb904233..0853ddb7 100644 --- a/lib/agent/constants/constants.go +++ b/lib/agent/constants/constants.go @@ -1,9 +1,23 @@ package constants +var ( + IsLambda bool + SocketPath = "/run/aikido-" + Version + "/aikido-agent.sock" + PidPath = "/run/aikido-" + Version + "/aikido-agent.pid" +) + +// SetRuntimeDir switches the socket/pid directory to /tmp when running on +// Lambda. The flag is passed from C++ via an argv (--lambda) when the agent +// is spawned. +func SetRuntimeDir(isLambda bool) { + if isLambda { + SocketPath = "/tmp/aikido-" + Version + "/aikido-agent.sock" + PidPath = "/tmp/aikido-" + Version + "/aikido-agent.pid" + } +} + const ( Version = "1.5.6" - SocketPath = "/run/aikido-" + Version + "/aikido-agent.sock" - PidPath = "/run/aikido-" + Version + "/aikido-agent.pid" ConfigUpdatedAtMethod = "GET" ConfigUpdatedAtAPI = "/config" ConfigAPIMethod = "GET" diff --git a/lib/agent/main.go b/lib/agent/main.go index 1198bf0e..71055a18 100644 --- a/lib/agent/main.go +++ b/lib/agent/main.go @@ -24,6 +24,12 @@ var serversCleanupChannel = make(chan struct{}) var serversCleanupTicker = time.NewTicker(time.Minute) func serversCleanupRoutine(_ *ServerData) { + // On Lambda, the agent process is frozen between invocations while + // wall-clock time keeps passing. Inactivity-based cleanup wrongly + // evicts the registered server and breaks subsequent requests. + if constants.IsLambda { + return + } for _, serverKey := range globals.GetServersKeys() { server := globals.GetServer(serverKey) if server == nil { @@ -88,6 +94,15 @@ func AgentUninit() { } func main() { + isLambda := false + for _, arg := range os.Args[1:] { + if arg == "--lambda" { + isLambda = true + break + } + } + constants.SetRuntimeDir(isLambda) + if !AgentInit() { log.Errorf(log.MainLogger, "Agent initialization failed!") os.Exit(-2) diff --git a/lib/php-extension/Agent.cpp b/lib/php-extension/Agent.cpp index 7ef0ee0a..2b4bcf0a 100644 --- a/lib/php-extension/Agent.cpp +++ b/lib/php-extension/Agent.cpp @@ -1,5 +1,12 @@ #include "Includes.h" +static std::string GetRuntimeDir() { + if (IsLambda()) { + return "/tmp/aikido-" + std::string(PHP_AIKIDO_VERSION); + } + return "/run/aikido-" + std::string(PHP_AIKIDO_VERSION); +} + vector Agent::GetPIDsFromRunningProcesses(const std::string& aikidoAgentPath) { vector agentPIDs; @@ -38,13 +45,16 @@ bool Agent::Start(std::string aikidoAgentPath) { posix_spawnattr_t attr; posix_spawnattr_init(&attr); - char* argv[] = { - const_cast(aikidoAgentPath.c_str()), - nullptr - }; + char lambdaFlag[] = "--lambda"; + std::vector argvVec; + argvVec.push_back(const_cast(aikidoAgentPath.c_str())); + if (IsLambda()) { + argvVec.push_back(lambdaFlag); + } + argvVec.push_back(nullptr); pid_t agentPid; - int status = posix_spawn(&agentPid, aikidoAgentPath.c_str(), nullptr, &attr, argv, nullptr); + int status = posix_spawn(&agentPid, aikidoAgentPath.c_str(), nullptr, &attr, argvVec.data(), nullptr); posix_spawnattr_destroy(&attr); if (status != 0) { AIKIDO_LOG_ERROR("Failed to start Aikido Agent process: %s\n", strerror(status)); @@ -105,7 +115,7 @@ bool Agent::IsRunning(const std::string& aikidoAgentPath, const std::string& aik AIKIDO_LOG_INFO("Found socket file \"%s\" on disk! Checking if Aikido Agent process is running...\n", aikidoAgentSocketPath.c_str()); - std::string aikidoAgentPidPath = "/run/aikido-" + std::string(PHP_AIKIDO_VERSION) + "/aikido-agent.pid"; + std::string aikidoAgentPidPath = GetRuntimeDir() + "/aikido-agent.pid"; pid_t agentPIDFromFile = this->GetPIDFromFile(aikidoAgentPidPath); vector agentPIDsFromRunningProcesses = this->GetPIDsFromRunningProcesses(aikidoAgentPath); if (agentPIDFromFile == -1 || @@ -126,7 +136,8 @@ bool Agent::IsRunning(const std::string& aikidoAgentPath, const std::string& aik bool Agent::Init() { std::string aikidoAgentPath = "/opt/aikido-" + std::string(PHP_AIKIDO_VERSION) + "/aikido-agent"; - std::string aikidoAgentSocketPath = "/run/aikido-" + std::string(PHP_AIKIDO_VERSION) + "/aikido-agent.sock"; + std::string runtimeDir = GetRuntimeDir(); + std::string aikidoAgentSocketPath = runtimeDir + "/aikido-agent.sock"; if (this->IsRunning(aikidoAgentPath, aikidoAgentSocketPath)) { AIKIDO_LOG_INFO("Aikido Agent is already running! Skipping init...\n"); @@ -140,6 +151,21 @@ bool Agent::Init() { return false; } + // On Lambda cold starts, MINIT and the first invoke happen back-to-back, + // so the first gRPC call can race against agent startup. Block here + // (up to ~5s) until the agent has bound its Unix socket. On regular + // long-running SAPIs (php-fpm, apache, frankenphp) MINIT runs well + // before any request, so this wait is unnecessary. + if (IsLambda()) { + for (int i = 0; i < 1000; i++) { + if (FileExists(aikidoAgentSocketPath)) { + AIKIDO_LOG_INFO("Aikido Agent socket ready after %d ms\n", i * 5); + return true; + } + usleep(5000); + } + AIKIDO_LOG_WARN("Aikido Agent socket did not appear within 1s\n"); + } return true; } diff --git a/lib/php-extension/RequestProcessor.cpp b/lib/php-extension/RequestProcessor.cpp index c728363f..fbc9a715 100644 --- a/lib/php-extension/RequestProcessor.cpp +++ b/lib/php-extension/RequestProcessor.cpp @@ -62,7 +62,7 @@ bool RequestProcessor::Init() { return false; } - if (!requestProcessorInitFn(GoCreateString(AIKIDO_GLOBAL(sapi_name)))) { + if (!requestProcessorInitFn(GoCreateString(AIKIDO_GLOBAL(sapi_name)), IsLambda())) { AIKIDO_LOG_ERROR("Failed to initialize Aikido Request Processor!\n"); this->initFailed = true; return false; diff --git a/lib/php-extension/Utils.cpp b/lib/php-extension/Utils.cpp index 1ad4b29c..c879bc0c 100644 --- a/lib/php-extension/Utils.cpp +++ b/lib/php-extension/Utils.cpp @@ -198,6 +198,10 @@ bool RemoveFile(const std::string& filePath) { return false; } +bool IsLambda() { + return getenv("AWS_LAMBDA_FUNCTION_NAME") != nullptr; +} + std::string GetStackTrace() { #if PHP_VERSION_ID >= 80100 diff --git a/lib/php-extension/include/RequestProcessor.h b/lib/php-extension/include/RequestProcessor.h index 2c77f157..e4312963 100644 --- a/lib/php-extension/include/RequestProcessor.h +++ b/lib/php-extension/include/RequestProcessor.h @@ -5,7 +5,7 @@ typedef GoUint8 (*InitInstanceFn)(void* instancePtr, GoString initJson); typedef void (*DestroyInstanceFn)(uint64_t threadId); // Updated typedefs with instance pointer as first parameter -typedef GoUint8 (*RequestProcessorInitFn)(GoString platformName); +typedef GoUint8 (*RequestProcessorInitFn)(GoString platformName, GoUint8 isLambda); typedef GoUint8 (*RequestProcessorContextInitFn)(void* instancePtr, ContextCallback); typedef GoUint8 (*RequestProcessorConfigUpdateFn)(void* instancePtr, GoString initJson); typedef char* (*RequestProcessorOnEventFn)(void* instancePtr, GoInt eventId); diff --git a/lib/php-extension/include/Utils.h b/lib/php-extension/include/Utils.h index ffdf98ff..8672959e 100644 --- a/lib/php-extension/include/Utils.h +++ b/lib/php-extension/include/Utils.h @@ -35,6 +35,8 @@ bool FileExists(const std::string& filePath); bool RemoveFile(const std::string& filePath); +bool IsLambda(); + std::string GetStackTrace(); zend_class_entry* GetFirewallDefaultExceptionCe(); \ No newline at end of file diff --git a/lib/request-processor/globals/globals.go b/lib/request-processor/globals/globals.go index b9ab154f..1167996d 100644 --- a/lib/request-processor/globals/globals.go +++ b/lib/request-processor/globals/globals.go @@ -79,6 +79,17 @@ func CreateServer(token string) *ServerData { } const ( - Version = "1.5.6" - SocketPath = "/run/aikido-" + Version + "/aikido-agent.sock" + Version = "1.5.6" ) + +var SocketPath = "/run/aikido-" + Version + "/aikido-agent.sock" + +// SetRuntimeDir switches the socket directory to /tmp when running on Lambda. +// The flag is passed from C++ (via RequestProcessorInit) because Go's +// os.LookupEnv is unreliable when this shared library is loaded into a +// forked FPM worker. +func SetRuntimeDir(isLambda bool) { + if isLambda { + SocketPath = "/tmp/aikido-" + Version + "/aikido-agent.sock" + } +} diff --git a/lib/request-processor/main.go b/lib/request-processor/main.go index 81a3ed06..618e76a5 100644 --- a/lib/request-processor/main.go +++ b/lib/request-processor/main.go @@ -80,7 +80,7 @@ func DestroyInstance(threadID uint64) { } //export RequestProcessorInit -func RequestProcessorInit(platformName string) (initOk bool) { +func RequestProcessorInit(platformName string, isLambda bool) (initOk bool) { defer func() { if r := recover(); r != nil { log.Warn(nil, "Recovered from panic:", r) @@ -88,6 +88,8 @@ func RequestProcessorInit(platformName string) (initOk bool) { } }() + globals.SetRuntimeDir(isLambda) + config.Init(platformName) if globals.EnvironmentConfig.PlatformName != "cli" { diff --git a/package/rpm/aikido.spec b/package/rpm/aikido.spec index 6c3f2203..6e2b1750 100644 --- a/package/rpm/aikido.spec +++ b/package/rpm/aikido.spec @@ -41,7 +41,7 @@ if command -v php -v >/dev/null 2>&1; then fi # Check common PHP installation paths -for php_path in /usr/bin/php* /usr/local/bin/php*; do +for php_path in /usr/bin/php* /usr/local/bin/php* /opt/bin/php*; do if [[ -x "$php_path" && "$php_path" =~ php([0-9]+\.[0-9]+)$ ]]; then version=$("$php_path" -v | grep -oP 'PHP \K\d+\.\d+' | head -n 1) if [[ ! " ${PHP_VERSIONS[@]} " =~ " ${version} " ]]; then @@ -133,9 +133,10 @@ for PHP_VERSION in "${PHP_VERSIONS[@]}"; do fi else # RedHat-based system - if [ -d "$PHP_MOD_DIR" ]; then - echo "Installing new Aikido mod in $PHP_MOD_DIR/zz-aikido-%{version}.ini..." - ln -sf /opt/aikido-%{version}/aikido.ini $PHP_MOD_DIR/zz-aikido-%{version}.ini + PHP_MOD_DIR_FIRST="${PHP_MOD_DIR%%:*}" + if [ -d "$PHP_MOD_DIR_FIRST" ]; then + echo "Installing new Aikido mod in $PHP_MOD_DIR_FIRST/zz-aikido-%{version}.ini..." + ln -sf /opt/aikido-%{version}/aikido.ini $PHP_MOD_DIR_FIRST/zz-aikido-%{version}.ini else echo "No mod dir for PHP $PHP_VERSION! Skipping..." continue @@ -198,7 +199,7 @@ if command -v php -v >/dev/null 2>&1; then fi # Check common PHP installation paths -for php_path in /usr/bin/php* /usr/local/bin/php*; do +for php_path in /usr/bin/php* /usr/local/bin/php* /opt/bin/php*; do if [[ -x "$php_path" && "$php_path" =~ php([0-9]+\.[0-9]+)$ ]]; then version=$("$php_path" -v | grep -oP 'PHP \K\d+\.\d+' | head -n 1) if [[ ! " ${PHP_VERSIONS[@]} " =~ " ${version} " ]]; then @@ -252,10 +253,11 @@ for PHP_VERSION in "${PHP_VERSIONS[@]}"; do rm -f $PHP_DEBIAN_MOD_DIR_APACHE2/zz-aikido-%{version}.ini fi else - # RedHat-based system - if [ -d "$PHP_MOD_DIR" ]; then - echo "Uninstalling Aikido mod from $PHP_MOD_DIR/zz-aikido-%{version}.ini..." - rm -f $PHP_MOD_DIR/zz-aikido-%{version}.ini + # RedHat-based system (take first dir if colon-separated) + PHP_MOD_DIR_FIRST="${PHP_MOD_DIR%%:*}" + if [ -f "$PHP_MOD_DIR_FIRST/zz-aikido-%{version}.ini" ]; then + echo "Uninstalling Aikido mod from $PHP_MOD_DIR_FIRST/zz-aikido-%{version}.ini..." + rm -f $PHP_MOD_DIR_FIRST/zz-aikido-%{version}.ini fi fi