Caching

I have a strong desire that hosting providers have some kind of secrets management available to make things simple. For now, I can only wish.

Today I improved upon my Secrets Manager focusing on optimization and security. One of the problems I don’t like about it is that it reads files with every request, and decrypts the values with each call. I’ve cached the values internally so that if the same value is requested again, it will pull the value from memory. However, this doesn’t persist in an application state. It’s only useful within the scope of the current request.

There are various workarounds to this.

  • $GLOBALS – this is only useful within the current request to access global variables.
  • File system – store data as a file. We are already doing this. We want to cache the data to skip the time it takes to read from a file system.
  • Session – Store in a session variable. Unfortunately sessions are still saved in temporary files, and I want this to persist application-wide.
  • Environment Variables – Once the .htaccess file is written, these values are read-only. We had just moved away from storing secrets in .htaccess.
  • Ram Disk – Store files in memory by saving to /mnt/tmpfs/file.ext
  • Shared Memory – Store data in a specific part of memory
  • APCu – Store in memory – server-wide
  • Memcache – Store in localhost memory, or another server
  • Memcached – Same as memcache, but with multi-key fetching, compression, and performance improvements
  • Redis – Similar to memcache but with complex data structures, data persistence, and replication
  • Database – store data in a database – our problem is that we are trying to cache database credentials, so we don’t have access to the database yet.

Ram Disk

This seemed like it needed me to mount a ram disk first before I could access it. Given that I’m on a shared host, I don’t know if that would be permitted, as I’m uncertain how they would identify that I’m the one using the RAM. If I had a virtual machine, it would be a different story. The server (or me) has 1 GB of ram available and usually consumes 14 MB. One of the problems I’ve had with Ram Disks are how to handle them when they are unmounted – ie, server reboot. Reading/Writing to the path would cause errors until someone logged in to create the ram disk again. Also – everyone on the server has access to the ram disk if they go looking for it.

sudo mount -t tmpfs -o size=2M tmpfs /mnt/tmpfs
<?php
function main()
{
  $tmpfsDir = '/mnt/tmpfs';
  $file = $tmpfsDir . '/example.txt';
  file_put_contents($file, 'Hello Yello!');
  
  $content = file_get_contents($file);
  echo "File: $content\n";
  
  unlink($file);  
}
try {
    main();
    echo "Done";
} catch (Exception $e) {
    echo "Exception: " . $e->getMessage();
}
?>

Shared Memory

Shared memory just seems a bit too low level for comfort. How long does the memory last? What happens if the value is too big? Should I always reserve the same number of bytes regardless of the data being stored? What about all of the wasted memory. Is the memory accessible by anything outside of PHP? In the end, it just seems dangerous to work with. Everyone on the server potentially has access to the memory if they go looking for it, but its so obscure and hard to work with, I doubt anyone uses it.

<?php
function main()
{
    $key = ftok(__FILE__, 'a');
    $shmId = shm_attach($key, 1024, 0666);
    if ($shmId === false) {
        echo "Failed to attach shared memory.";
        return;
    }

    $data = shm_get_var($shmId, 1);

    if ($data === false) {
        $data = "This is the cached data.";
        shm_put_var($shmId, 1, $data);
        echo "Data added to shared memory.";
    } else {
        echo "Cached data: $data";
    }

    shm_detach($shmId);
}
try {
    main();
    echo "Done";
} catch (Exception $e) {
    echo "Exception: " . $e->getMessage();
}
?>

Redis

Redis

I’ve heard of Redis in the past and I’ve been asked about my familiarity with it a few years ago for a potential project. Unfortunately, my host doesn’t offer an extension for it. Hostinger just posted an article about installing Redis last month, but it’s for their virtual hosts. Not only would I need to install a Redis server, but I would need to manually install the Redis php extension to use it as well.

If I had a Redis server setup somewhere else, I could use sockets to communicate with it over tcp/ip (fsockopen, fgets, fwrite, fread, fclose). With a bare-bones approach, it appears that you connect and send SET NAME VALUE and GET NAME commands. The responses come with a prefix (+-:$*) to tell you if you have a string, error, integer, bulk string, or array. The bulk strings and arrays are a bit tricky since they require additional reads. I wouldn’t be surprised if a package was available via composer that had all of the communications worked out.

It’s doable – I just don’t have anything setup at the moment, and my host doesn’t offer it.

APC User Cache

APCu

I tried out APCu (Alternative PHP Cache User). I enabled the extension and experimented with it. I soon realized that both of my sub-domains were competing with the same pool of keys. At first, I decided to just hash the key names with the $_SERVER["SERVER_NAME"] as a prefix. This worked in the fact that I was able to access the correct keys on each sub-domain. Then I started wondering about who may have access to the keys. I proceeded to store the encrypted values of the keys rather than the clear text. Unfortunately, that means we’ve got a bit more computing involved to decrypt them. I started thinking – can anyone else access my keys? How could I check?

I went further and setup a new subdomain “blog” as a separate site rather than under the other four sets of app, api, dev-app, and dev-api. It was on the same server, but I could change things around such as the PHP version and file extensions. I had to go in and enable apcu for the blog website as well, and found that once I did, it had access to the same keys.

This isn’t good. Even if my data was semi-safe being encrypted, someone could still change them via apcu_store. The keys are available to anyone if you call apcu_cache_info.

<?php
if (extension_loaded('apcu')) {
    $cacheInfo = apcu_cache_info();
    if ($cacheInfo !== false) {
        $cacheKeys = array_column($cacheInfo['cache_list'], 'info');
        foreach ($cacheKeys as $key) {
            $value = apcu_fetch($key);
            if ($value !== false) {
                echo "Key: $key, Value: $value<br />\n";
            } else {
                echo "Failed to fetch value for key: $key\n";
            }
        }
        echo "done";
    } else {
        echo "Failed to get cache information\n";
    }
} else {
    echo "APCu extension is not loaded.\n";
}
?>

Memcache

Memcached

My host only offers memcache and even calls it out as a legacy extension. It has since been superseded by MemcacheD, but my host doesn’t offer it.

From what I can see, at one time you could request all keys by getting the extended status from cachedump for a specific slab. This is no longer the case. This helps with the Security aspect where no one is able to see a list of keys that they can access. Since no one can see the keys, should I store the data as encrypted, or the raw value? The difference is the amount of resources (cpu/memory) used to decrypt the data. For now, I’m encrypting everything in cache. I’d prefer to use memcached instead since memcache is outdated and no longer supported.

I created a sample page to test it out and placed it on each host.

<?php
$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die("Could not connect to Memcache");
$key = 'key';
$cachedValue = $memcache->get($key);
if ($cachedValue) {
    echo "Retrieved value from cache: " . $cachedValue . "<br>";
} else {
    echo "Value not found in cache.<br>";
}
echo "Setting to server";
$memcache->set($key, $_SERVER["SERVER_NAME"], false, 3600);
$memcache->close();
?>

As I suspected, each domain was able to read the same memcache values, and update the value. This could be beneficial if I needed a central location to store values or would like to move the caching to another server. The fact that no one else can read what keys I have added is beneficial. I can use the same hashing algorithm from the APCu to ensure my keys don’t collide with anyone else. The following functions are used to cache my secrets:

private static function cache_available()
{
    $available = self::$use_cache &&
    extension_loaded('memcache');
    if (!$available) {
        return false;
    }

    self::$cache = new Memcache;
    $result = self::$cache->connect('localhost', 11211);
    if ($result === false) {
        throw new Exception("Memcache failed");
    }
    return true;
}
private static function cache_name($name)
{
    // apcu - all php websites on same host server can see keys
    // memcache - all websites have same access, but can't see keys
    return hash('sha256', 
      $_SERVER['SERVER_NAME'] . 
      getenv("SECRETS_PEPPER") . 
      $name
    );
}
private static function get_cache($name)
{
    if (self::cache_available()) {
        return self::$cache->get($name);
    }
    return false;
}
private static function set_cache($name, #[SensitiveParameter] $value)
{
    if (self::cache_available()) {
        $success = self::$cache->set($name, $value);
        return $success;
    }
    return false;
}

One response to “Caching”

Discover more from Lewis Moten

Subscribe now to keep reading and get access to the full archive.

Continue reading