"Another open source project dies of neglect, leaving thousands scrambling"
#OpenSource #NGINX #Kubernettes
https://www.theregister.com/2025/12/02/ingress_nginx_opinion/
#Tag
"Another open source project dies of neglect, leaving thousands scrambling"
#OpenSource #NGINX #Kubernettes
https://www.theregister.com/2025/12/02/ingress_nginx_opinion/
"Another open source project dies of neglect, leaving thousands scrambling"
#OpenSource #NGINX #Kubernettes
https://www.theregister.com/2025/12/02/ingress_nginx_opinion/
https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/
Any computer program can be designed to run from a single file if you architect it wrong enough!
I wanted to create the simplest possible Fediverse server which can be used as an educational tool to show how ActivityPub / Mastodon works.
The design goals were:
And those goals have all been met! Check it out on GitLab. I warn you though, it is the nadir of bad coding. There are no tests, bugger-all security, scalability isn't considered, and it is a mess. But it works.
You can follow the test user @example@example.viii.fi
Firstly, I've slightly cheated on my "single file" stipulation. There's an .htaccess file which turns example.com/whatever into example.com/index.php?path=whatever
The index.php file then takes that path and does stuff. It also contains all the configuration variables which is very bad practice.
Rather than using a database, it saves files to disk.
Again, this is not suitable for any real world use. This is an educational tool to help explain the basics of posting messages to the Fediverse. It requires absolutely no dependencies. You do not need to spin up a dockerised hypervisor to manage your node bundles and re-compile everything to WASM. Just FTP the file up to prod and you're done.
This is a quick ramble through the code. It is reasonably well documented, I hope.
This is where you set up your account's name and bio. You also need to provide a public/private keypair. The posting page is protected with a password that also needs to be set here.
PHP
// Set up the Actor's information $username = rawurlencode("example"); // Encoded as it is often used as part of a URl $realName = "E. Xample. Jr."; $summary = "Some text about the user."; $server = $_SERVER["SERVER_NAME"]; // Domain name this is hosted on // Generate locally or from https://cryptotools.net/rsagen // Newlines must be replaced with "\n" $key_private = "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"; $key_public = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"; // Password for sending messages $password = "P4ssW0rd";
ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them in /logs/ as a datestamped text file.
PHP
// Get all headers and requests sent to this server $headers = print_r( getallheaders(), true ); $postData = print_r( $_POST, true ); $getData = print_r( $_GET, true ); $filesData = print_r( $_FILES, true ); $body = json_decode( file_get_contents( "php://input" ), true ); $bodyData = print_r( $body, true ); $requestData = print_r( $_REQUEST, true ); $serverData = print_r( $_SERVER, true ); // Get the type of request - used in the log filename if ( isset( $body["type"] ) ) { $type = " " . $body["type"]; } else { $type = ""; } // Create a timestamp in ISO 8601 format for the filename $timestamp = date( "c" ); // Filename for the log $filename = "{$timestamp}{$type}.txt"; // Save headers and request data to the timestamped file in the logs directory if( ! is_dir( "logs" ) ) { mkdir( "logs"); } file_put_contents( "logs/{$filename}", "Headers: \n$headers \n\n" . "Body Data: \n$bodyData \n\n" . "POST Data: \n$postData \n\n" . "GET Data: \n$getData \n\n" . "Files Data: \n$filesData \n\n" . "Request Data:\n$requestData\n\n" . "Server Data: \n$serverData \n\n" );
The .htaccess changes /whatever to /?path=whateverThis runs the function of the path requested.
PHP
!empty( $_GET["path"] ) ? $path = $_GET["path"] : die(); switch ($path) { case ".well-known/webfinger": webfinger(); case rawurldecode( $username ): username(); case "following": following(); case "followers": followers(); case "inbox": inbox(); case "write": write(); case "send": send(); default: die(); }
The WebFinger Protocol is used to identify accounts.It is requested with example.com/.well-known/webfinger?resource=acct:username@example.comThis server only has one user, so it ignores the query string and always returns the same details.
PHP
function webfinger() { global $username, $server; $webfinger = array( "subject" => "acct:{$username}@{$server}", "links" => array( array( "rel" => "self", "type" => "application/activity+json", "href" => "https://{$server}/{$username}" ) ) ); header( "Content-Type: application/json" ); echo json_encode( $webfinger ); die(); }
Requesting example.com/username returns a JSON document with the user's information.
PHP
function username() { global $username, $realName, $summary, $server, $key_public; $user = array( "@context" => [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" ], "id" => "https://{$server}/{$username}", "type" => "Person", "following" => "https://{$server}/following", "followers" => "https://{$server}/followers", "inbox" => "https://{$server}/inbox", "preferredUsername" => rawurldecode($username), "name" => "{$realName}", "summary" => "{$summary}", "url" => "https://{$server}", "manuallyApprovesFollowers" => true, "discoverable" => true, "published" => "2024-02-12T11:51:00Z", "icon" => [ "type" => "Image", "mediaType" => "image/png", "url" => "https://{$server}/icon.png" ], "publicKey" => [ "id" => "https://{$server}/{$username}#main-key", "owner" => "https://{$server}/{$username}", "publicKeyPem" => $key_public ] ); header( "Content-Type: application/activity+json" ); echo json_encode( $user ); die(); }
These JSON documents show how many users are following / followers-of this account.The information here is self-attested. So you can lie and use any number you want.
PHP
function following() { global $server; $following = array( "@context" => "https://www.w3.org/ns/activitystreams", "id" => "https://{$server}/following", "type" => "Collection", "totalItems" => 0, "items" => [] ); header( "Content-Type: application/activity+json" ); echo json_encode( $following ); die(); } function followers() { global $server; $followers = array( "@context" => "https://www.w3.org/ns/activitystreams", "id" => "https://{$server}/followers", "type" => "Collection", "totalItems" => 0, "items" => [] ); header( "Content-Type: application/activity+json" ); echo json_encode( $followers ); die(); }
The /inbox is the main server. It receives all requests. This server only responds to "Follow" requests.A remote server sends a follow request which is a JSON file saying who they are.This code does not cryptographically validate the headers of the received message.The name of the remote user's server is saved to a file so that future messages can be delivered to it.An accept request is cryptographically signed and POST'd back to the remote server.
PHP
function inbox() { global $body, $server, $username, $key_private; // Get the message and type $inbox_message = $body; $inbox_type = $inbox_message["type"]; // This inbox only responds to follow requests if ( "Follow" != $inbox_type ) { die(); } // Get the parameters $inbox_id = $inbox_message["id"]; $inbox_actor = $inbox_message["actor"]; $inbox_host = parse_url( $inbox_actor, PHP_URL_HOST ); // Does this account have any followers? if( file_exists( "followers.json" ) ) { $followers_file = file_get_contents( "followers.json" ); $followers_json = json_decode( $followers_file, true ); } else { $followers_json = array(); } // Add user to list. Don't care about duplicate users, server is what's important $followers_json[$inbox_host]["users"][] = $inbox_actor; // Save the new followers file file_put_contents( "followers.json", print_r( json_encode( $followers_json ), true ) ); // Response Message ID // This isn't used for anything important so could just be a random number $guid = uuid(); // Create the Accept message $message = [ "@context" => "https://www.w3.org/ns/activitystreams", "id" => "https://{$server}/{$guid}", "type" => "Accept", "actor" => "https://{$server}/{$username}", "object" => [ "@context" => "https://www.w3.org/ns/activitystreams", "id" => $inbox_id, "type" => $inbox_type, "actor" => $inbox_actor, "object" => "https://{$server}/{$username}", ] ]; // The Accept is sent to the server of the user who requested the follow // TODO: The path doesn't *always* end with/inbox $host = $inbox_host; $path = parse_url( $inbox_actor, PHP_URL_PATH ) . "/inbox"; // Get the signed headers $headers = generate_signed_headers( $message, $host, $path ); // Specify the URL of the remote server's inbox // TODO: The path doesn't *always* end with /inbox $remoteServerUrl = $inbox_actor . "/inbox"; // POST the message and header to the requester's inbox $ch = curl_init( $remoteServerUrl ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" ); curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode($message) ); curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); $response = curl_exec( $ch ); // Check for errors if( curl_errno( $ch ) ) { file_put_contents( "error.txt", curl_error( $ch ) ); } curl_close($ch); die(); }
Every message sent should have a unique ID. This can be anything you like. Some servers use a random number.I prefer a date-sortable string.
PHP
function uuid() { return sprintf( "%08x-%04x-%04x-%04x-%012x", time(), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffffffffffff) ); }
Every message that your server sends needs to be cryptographically signed with your Private Key.This is a complicated process. Please read "How to make friends and verify requests" for more information.
PHP
function generate_signed_headers( $message, $host, $path ) { global $server, $username, $key_private; // Encode the message to JSON $message_json = json_encode( $message ); // Location of the Public Key $keyId = "https://{$server}/{$username}#main-key"; // Generate signing variables $hash = hash( "sha256", $message_json, true ); $digest = base64_encode( $hash ); $date = date( "D, d M Y H:i:s \G\M\T" ); // Get the Private Key $signer = openssl_get_privatekey( $key_private ); // Sign the path, host, date, and digest $stringToSign = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest"; // The signing function returns the variable $signature // https://www.php.net/manual/en/function.openssl-sign.php openssl_sign( $stringToSign, $signature, $signer, OPENSSL_ALGO_SHA256 ); // Encode the signature $signature_b64 = base64_encode( $signature ); // Full signature header $signature_header = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"'; // Header for POST reply $headers = array( "Host: {$host}", "Date: {$date}", "Digest: SHA-256={$digest}", "Signature: {$signature_header}", "Content-Type: application/activity+json", "Accept: application/activity+json", ); return $headers; }
This creates a basic HTML form. Type in your message and your password. It then POSTs the data to the /send endpoint.
PHP
function write() { // Display an HTML form for the user to enter a message.echo <<< HTML<!DOCTYPE html><html lang="en-GB"> <head> <meta charset="UTF-8"> <title>Send Message</title> <style> *{font-family:sans-serif;font-size:1.1em;} </style> </head> <body> <form action="/send" method="post" enctype="multipart/form-data"> <label for="content">Your message:</label><br> <textarea id="content" name="content" rows="5" cols="32"></textarea><br> <label for="password">Password</label><br> <input type="password" name="password" id="password" size="32"><br> <input type="submit" value="Post Message"> </form> </body></html>HTML; die(); }
This takes the submitted message and checks the password is correct.It reads the followers.json file and sends the message to every server that is following this account.
PHP
function send() { global $password, $server, $username, $key_private; // Does the posted password match the stored password? if( $password != $_POST["password"] ) { die(); } // Get the posted content $content = $_POST["content"]; // Current time - ISO8601 $timestamp = date( "c" ); // Outgoing Message ID $guid = uuid(); // Construct the Note // contentMap is used to prevent unnecessary "translate this post" pop ups // hardcoded to English $note = [ "@context" => array( "https://www.w3.org/ns/activitystreams" ), "id" => "https://{$server}/posts/{$guid}.json", "type" => "Note", "published" => $timestamp, "attributedTo" => "https://{$server}/{$username}", "content" => $content, "contentMap" => ["en" => $content], "to" => ["https://www.w3.org/ns/activitystreams#Public"] ]; // Construct the Message $message = [ "@context" => "https://www.w3.org/ns/activitystreams", "id" => "https://{$server}/posts/{$guid}.json", "type" => "Create", "actor" => "https://{$server}/{$username}", "to" => [ "https://www.w3.org/ns/activitystreams#Public" ], "cc" => [ "https://{$server}/followers" ], "object" => $note ]; // Create the context for the permalink $note = [ "@context" => "https://www.w3.org/ns/activitystreams", ...$note ]; // Save the permalink $note_json = json_encode( $note ); // Check for posts/ directory and create it if( ! is_dir( "posts" ) ) { mkdir( "posts"); } file_put_contents( "posts/{$guid}.json", print_r( $note_json, true ) ); // Read existing users and get their hosts $followers_file = file_get_contents( "followers.json" ); $followers_json = json_decode( $followers_file, true ); $hosts = array_keys( $followers_json ); // Prepare to use the multiple cURL handle $mh = curl_multi_init(); // Loop through all the severs of the followers // Each server needs its own cURL handle // Each POST to an inbox needs to be signed separately foreach ( $hosts as $host ) { $path = "/inbox"; // Get the signed headers $headers = generate_signed_headers( $message, $host, $path ); // Specify the URL of the remote server $remoteServerUrl = "https://{$host}{$path}"; // POST the message and header to the requester's inbox $ch = curl_init( $remoteServerUrl ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" ); curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode($message) ); curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); // Add the handle to the multi-handle curl_multi_add_handle( $mh, $ch ); } // Execute the multi-handle do { $status = curl_multi_exec( $mh, $active ); if ( $active ) { curl_multi_select( $mh ); } } while ( $active && $status == CURLM_OK ); // Close the multi-handle curl_multi_close( $mh ); // Render the JSON so the user can see the POST has worked header( "Location: https://{$server}/posts/{$guid}.json" ); die(); }
This is not intended to be used in production. Ever. But if you would like to contribute more simple examples of how the protocol works, please come and play on GitLab.
You can follow the test user @example@example.viii.fi
I discover your onepage activitypub application at https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/
It is really a good start for me
But how would you adapt it with Ngnix server
All the logic is based on .htaccess but in Nginx it leads with all 404 pages
Looking for some help with interaction issues between NGINX, HTMX, and GoToSocial.
My GET request from my website HaugenHus | Now Test
hx-get="https://fedi.haugenh.us/@kaleb/feed.rss"
hx-headers='{"Accept": "application/feed+json"}'
…is returning two basic CORS errors:
…that I can’t figure out how to resolve.
I ’ve added
add_header 'Access-Control-Allow-Origin' 'https://fedi.haugenh.us';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, Accept, HX-Request';
to my nginx.conf (actually I’ve imported it from another .conf and verified that the config is being included with nginx -T), but it has no impact on the errors.
I have confirmed that the page loads successfully including the remote JSON when I set my browser to ignore CORS.
Additionally, I am able to successfully load the remote feed.rss when I use the hx-request='{"noHeaders": true}’, but doing so returns XML rather than the desired JSON due the JSON Feed implementation in GoToSocial RSS
Kubernetes Ingress Nginx is retiring
https://www.kubernetes.dev/blog/2025/11/12/ingress-nginx-retirement/
#HackerNews #Kubernetes #Ingress #Nginx #retirement #Kubernetes #Nginx #Ingress #Kubernetes #updates #Cloud-native #technologies #DevOps
Hi,
I'm looking for someone who will help provide some technical troubleshooting with certbot and nginx.
The hardening considerations are interfering with certbot.
Boosts appreciated.
#nginx #webdev #foss #selfhosted
#techsupport #support
Hi,
I'm looking for someone who will help provide some technical troubleshooting with certbot and nginx.
The hardening considerations are interfering with certbot.
Boosts appreciated.
#nginx #webdev #foss #selfhosted
#techsupport #support
A #linux application runs inside an LX-branded zone, where the #Illumos kernel (descended from #Solaris) natively implements the Linux kernel ABI. A #VPN connection, terminated in a separate Solaris-style zone, routes the application's traffic to a VPN endpoint on an #OpenBSD system running #Nginx, which serves #HTTPS content on a custom domain.
Life is great!
A #linux application runs inside an LX-branded zone, where the #Illumos kernel (descended from #Solaris) natively implements the Linux kernel ABI. A #VPN connection, terminated in a separate Solaris-style zone, routes the application's traffic to a VPN endpoint on an #OpenBSD system running #Nginx, which serves #HTTPS content on a custom domain.
Life is great!
I've got the skeleton of a companion container for nginx-proxy/nginx-proxy that will use RFC2136 to add/remove CNAME entries for proxied containers as they are created/removed.
I'll push it to GitHub from my Forgejo instance once it's a little bit more polished
This is in a similar vein to my one that adds mdns/avahi CNAME to the host machine.
https://github.com/hardillb/nginx-proxy-avahi-helper
I've got the skeleton of a companion container for nginx-proxy/nginx-proxy that will use RFC2136 to add/remove CNAME entries for proxied containers as they are created/removed.
I'll push it to GitHub from my Forgejo instance once it's a little bit more polished
This is in a similar vein to my one that adds mdns/avahi CNAME to the host machine.
https://github.com/hardillb/nginx-proxy-avahi-helper
What #OpenSource and #SelfHost can do. Had an idea, discussed it here. Seemed to rhyme with people. Booked two domains. Created a landing page with #Jekyll and CI/CD from a #git repo on my #Forgejo instance. Created logo with #Inkscape. Added #letsencrypt certificate. Put it on my VPS (Virtual Private Server) running Red Hat Enterprise Linux, (#RHEL) where it is now served with #Nginx. Git repo mirrored to #Codeberg so all can join. In under 8h.
Does anyone know of a public set of ModSecurity exceptions for the fediverse/ActivityPub I can take a look at? I'm setting it up for GoToSocial and Mastodon now and manually doing this is pain.
Update, @cloudymax and I started a plugin here:
https://github.com/small-hack/argocd-apps/blob/2b7995c6fae5ecbb3944c6c6f4b139d98b76e67f/ingress-nginx/modsecurity_plugins_configmap.yaml#L177
Still happy to collaborate on it, but also wanted to note there was a mention a year ago about making an ActivityPub plugin over at the OWASP CRS repo, so maybe we could donate to that if its ever created:
https://github.com/coreruleset/coreruleset/issues/3497#issuecomment-1902181156
#WAF #modsecurity #nginx #apache #firewall #webApplicationFirewall #mastodon #gotosocial #activitypub
Does anyone know of a public set of ModSecurity exceptions for the fediverse/ActivityPub I can take a look at? I'm setting it up for GoToSocial and Mastodon now and manually doing this is pain.
Update, @cloudymax and I started a plugin here:
https://github.com/small-hack/argocd-apps/blob/2b7995c6fae5ecbb3944c6c6f4b139d98b76e67f/ingress-nginx/modsecurity_plugins_configmap.yaml#L177
Still happy to collaborate on it, but also wanted to note there was a mention a year ago about making an ActivityPub plugin over at the OWASP CRS repo, so maybe we could donate to that if its ever created:
https://github.com/coreruleset/coreruleset/issues/3497#issuecomment-1902181156
#WAF #modsecurity #nginx #apache #firewall #webApplicationFirewall #mastodon #gotosocial #activitypub
What #OpenSource and #SelfHost can do. Had an idea, discussed it here. Seemed to rhyme with people. Booked two domains. Created a landing page with #Jekyll and CI/CD from a #git repo on my #Forgejo instance. Created logo with #Inkscape. Added #letsencrypt certificate. Put it on my VPS (Virtual Private Server) running Red Hat Enterprise Linux, (#RHEL) where it is now served with #Nginx. Git repo mirrored to #Codeberg so all can join. In under 8h.
https://github.com/nginx/nginx/pull/840
If you want to see ECH in nginx sooner rather than later, please jump in and review, give feedback, thumbs up, etc.
https://github.com/nginx/nginx/pull/840
If you want to see ECH in nginx sooner rather than later, please jump in and review, give feedback, thumbs up, etc.
A space for Bonfire maintainers and contributors to communicate