DEV Community

Cover image for Presigned URLs and Secure Direct Uploads with Lioran S3
Swaraj Puppalwar
Swaraj Puppalwar

Posted on

Presigned URLs and Secure Direct Uploads with Lioran S3

Presigned URLs and Secure Direct Uploads with Lioran S3

Your frontend usually should not receive your storage administrator password.

That sentence is obvious enough to print on a coffee mug, yet credentials still somehow end up inside browser bundles.

Lioran S3 V1 Pre-Alpha provides expiring signed URLs for delegated GET, PUT, and HEAD operations.

It is built by Lioran Developer Solutions under Lioran Group, led by Swaraj Puppalwar.

The pattern

A trusted backend authenticates to Lioran S3.

The backend generates a short-lived signed URL.

The untrusted or less-trusted client receives only that URL.

Browser
   |
   | asks backend for upload permission
   v
Application Backend
   |
   | authenticated storage request
   v
Lioran S3
   |
   | returns expiring signed URL
   v
Application Backend
   |
   | sends URL
   v
Browser
   |
   | direct PUT
   v
Lioran S3
Enter fullscreen mode Exit fullscreen mode

Create a client

import {
  BastionClient,
} from "@liorans3/driver";

const client = new BastionClient(
  process.env.LIORAN_S3_URI!
);

const bucket =
  client.bucket("uploads");
Enter fullscreen mode Exit fullscreen mode

Signed GET

const url =
  await bucket.presignGet(
    "private/report.pdf",
    {
      expiresIn: 3600,
    }
  );

console.log(url);
Enter fullscreen mode Exit fullscreen mode

The caller can temporarily download that object without receiving the backend's credentials.

Signed PUT

const uploadUrl =
  await bucket.presignPut(
    "incoming/avatar.png",
    {
      expiresIn: 900,
    }
  );
Enter fullscreen mode Exit fullscreen mode

Then your frontend can upload directly:

const file =
  input.files?.[0];

if (!file) {
  throw new Error("No file selected");
}

await fetch(uploadUrl, {
  method: "PUT",
  body: file,
  headers: {
    "content-type":
      file.type ||
      "application/octet-stream",
  },
});
Enter fullscreen mode Exit fullscreen mode

Signed HEAD

const headUrl =
  await bucket.presignHead(
    "private/report.pdf",
    {
      expiresIn: 600,
    }
  );
Enter fullscreen mode Exit fullscreen mode

This is useful when a client needs to verify object metadata without downloading the payload.

Backend route example

import express from "express";
import {
  BastionClient,
} from "@liorans3/driver";

const app = express();

const storage =
  new BastionClient(
    process.env.LIORAN_S3_URI!
  );

app.post(
  "/api/uploads/avatar",
  async (req, res) => {
    // Authenticate your application user first.

    const objectKey =
      `avatars/${crypto.randomUUID()}.png`;

    const bucket =
      storage.bucket("users");

    const uploadUrl =
      await bucket.presignPut(
        objectKey,
        {
          expiresIn: 5 * 60,
        }
      );

    res.json({
      objectKey,
      uploadUrl,
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

The browser never sees:

  • admin password
  • access-key secret
  • storage connection URI

Expiration matters

Do not issue week-long upload URLs when five minutes is enough.

Use the shortest practical lifetime.

await bucket.presignPut(
  key,
  {
    expiresIn: 300,
  }
);
Enter fullscreen mode Exit fullscreen mode

Server signing secret

Lioran S3 signs delegated URLs using an HMAC signing secret.

Production deployments should configure a persistent, high-entropy signing secret so URLs behave consistently across process restarts.

For example:

BASTION_SIGNING_SECRET=replace-with-a-long-random-secret
Enter fullscreen mode Exit fullscreen mode

Do not publish it.

Maximum TTL

The server also supports a maximum signed-URL TTL.

This lets infrastructure operators enforce an upper bound even if an application asks for something excessively long.

CORS

Direct browser uploads require correct CORS configuration.

Production should restrict allowed origins:

BASTION_CORS_ORIGINS=https://app.example.com
Enter fullscreen mode Exit fullscreen mode

Avoid a permissive production origin policy unless your threat model genuinely permits it.

Access keys for backend services

Instead of a human user's password, a service can authenticate using programmatic access keys.

const client =
  new BastionClient({
    accessKey:
      process.env.LIORAN_ACCESS_KEY!,
    secretKey:
      process.env.LIORAN_SECRET_KEY!,
    host:
      "storage.example.com",
    isTls: true,
  });
Enter fullscreen mode Exit fullscreen mode

That makes secret rotation easier and separates automation credentials from human login credentials.

Security checklist

For production-style evaluation:

  • terminate public traffic over HTTPS
  • rotate bootstrap admin credentials
  • keep the signing secret stable and private
  • restrict CORS
  • use short signed-URL lifetimes
  • do not expose access-key secrets to browsers
  • rotate compromised access keys
  • log identifiers, not raw secrets
  • avoid embedding passwords in source code

Signed URLs are small primitives, but they dramatically improve object-storage integration because your application backend stops being a mandatory data proxy.

Docs: https://docs.liorans3.sbs

Top comments (0)