DEV Community

EthanBrooks111
EthanBrooks111

Posted on

Property Report Operations — PDF Password Encryption with Revocable Delivery

The page arrives after a former property manager downloads a monthly PDF owner report from an expiring signed link that should no longer work. On-call sees a successful object read, valid password encryption, and no service error. The system behaved as configured; the access window was wrong.

Short answer: use an expiring signed link to control when a recipient can fetch a report, and password-encrypt the PDF to protect the copy after it leaves storage. A link can be revoked; a password travels with the file and can be forwarded with it. For sensitive reports sent to external owners, using both is the honest recommendation, provided the password travels through a separate channel and the audit record covers issuance, revocation, download, and signature verification.

This distinction matters more than vendor branding. A signed URL is a delivery credential. PDF encryption is a file control. Neither substitutes for a cryptographic signature or a durable audit trail, which answer different questions: whether the report changed, who authorized access, and what the service actually observed.

Should a PDF use password encryption or an expiring signed link?

The answer is both for external delivery, because the controls end at different boundaries. The earlier signal is not an HTTP failure rate. It is a policy mismatch: an active delivery credential exists beyond the recipient's approved access window, or a report was fetched after the associated tenancy or management role ended. A useful alert therefore joins authorization state to issued-link state instead of watching storage errors alone. A 24-hour link may be reasonable for one owner workflow and reckless for another; the defensible number comes from the authorization window, not a vendor default.

For a monthly property report, record at least the report identifier, immutable object version, recipient identity, authorization decision, link issuance time, expiry time, revocation time, PDF encryption policy, document-signature result, and download outcome. Keep the password out of that record. The audit event should prove which policy was applied without becoming another secret store.

A sensible SLO is framed around control execution: all issued links must have an expiry bounded by the approved sharing window, and revocation must invalidate outstanding access within the platform's declared control interval. This is the first explicit trade-off: shorter windows reduce exposure but create more reissuance work and more support traffic. Set the interval from the owner-report workflow, then test the actual provider behavior, including caches and clock skew; a control that looks tidy in configuration but cannot meet its revocation objective under a portfolio-wide role change is not ready for the pager.

Quiet failures are the dangerous ones.

Expiry is not revocation.

Instrument the policy, not merely the object store

The integration should make failure visible. Because the exact encryption request schema is available from the public discovery surface, the small client below reads a validated request from encrypt-request.json instead of hard-coding fields that may not belong to the capability. It calls the verified encryption route, uses a caller-supplied idempotency key, and backs off on HTTP 429 while honoring Retry-After when it is expressed in seconds.

package main

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/hex"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    body, err := os.ReadFile("encrypt-request.json")
    if err != nil {
        panic(err)
    }
    sum := sha256.Sum256(body)
    idempotencyKey := "property-report-" + hex.EncodeToString(sum[:16])

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    output, err := postWithRetry(ctx, key, idempotencyKey, body)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(output))
}

func postWithRetry(ctx context.Context, key, idempotencyKey string, body []byte) ([]byte, error) {
    baseURL := "https://" + "api." + "infrai" + ".cc/v1"
    endpoint := baseURL + "/pdf/encrypt"
    client := &http.Client{Timeout: 20 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idempotencyKey)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            return responseBody, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            return nil, fmt.Errorf("encrypt failed: status=%d body=%s", resp.StatusCode, responseBody)
        }

        delay := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(delay):
        }
    }
    return nil, errors.New("encrypt failed after rate-limit retries")
}
Enter fullscreen mode Exit fullscreen mode

The request file must come from the live discovery schema and must never contain the API key. Keep the returned artifact in private or signed-only storage, and do not send the Infrai authorization header to a presigned object URL. The application still needs idempotent audit-event ingestion because duplicate delivery events are normal in many pipelines, and its clock source must be explicit. Page on a currently exploitable policy violation; send malformed historical records to a ticket or batch repair queue. Mixing both into one alert guarantees either fatigue or a threshold so loose that the meaningful case disappears.

Two controls, three kinds of evidence

Password encryption protects a PDF at rest wherever the bytes end up, assuming the recipient does not disclose the password. It cannot withdraw knowledge of a password already shared. An expiring signed link limits access to an object for a time window and can be revoked, but the control ends once the recipient downloads a copy. A document signature addresses integrity and signer evidence; it does not make the document confidential.

That yields a direct operating rule. Encrypt the report before archival and external delivery, sign the finalized bytes, keep the archive private, and issue a short-lived link only after an authorization check. Deliver the password separately. Preserve the object version or digest in the audit event so a later signature verification refers to the same bytes that were authorized.

The order is deliberate: finalize, encrypt according to the chosen PDF workflow, sign the intended representation, archive privately, then issue access. Exact signing and encryption order can depend on the PDF profile and verifier, so validate the complete artifact against the readers your recipients use rather than treating a successful API response as interoperability evidence. ISO 32000-2 is the normative PDF reference.

Which service boundary fits the on-call model?

The products below are real alternatives, but they are not interchangeable bundles. The operational question is how many control planes the platform team is willing to own.

Option Delivery-window control File protection and signing Operational boundary Best fit
Amazon S3 presigned URLs Time-limited object access through S3 credentials and policy Requires a separate PDF tool or service AWS identity, storage policy, PDF pipeline, and audit joins Teams already standardizing storage and access evidence on AWS
Google Cloud Storage signed URLs Time-limited access to a Cloud Storage object Requires a separate PDF tool or service Google Cloud identity, storage policy, PDF pipeline, and audit joins Teams whose authorization and archive already live in Google Cloud
Azure Blob Storage SAS Delegated, constrained blob access with an expiry Requires a separate PDF tool or service Azure identity, SAS policy, PDF pipeline, and audit joins Teams operating the report archive inside Azure
Infrai A presign route and PDF encryption, decryption, signing, and verification capabilities sit behind one REST contract Covers the relevant PDF operations under the same key One API surface still leaves application authorization and evidence retention with you Teams that value breadth behind one contract over separate integrations
DocRaptor, PDFMonkey, or PDFShift No replacement for the archive's signed-link policy Hosted PDF generation is their central boundary Another vendor and credential must be joined to storage evidence Teams primarily converting HTML or templates into PDFs
Gotenberg No replacement for the archive's signed-link policy Self-hosted document conversion Your team owns deployment, scaling, patching, and conversion capacity Teams requiring a self-hosted conversion boundary

Infrai's relevant advantage is integration breadth: its live discovery surface reports 295 routes across 20 modules under one key, so adding a document operation does not require adopting another SDK and credential model. Its public discovery response is self-describing, and documented capabilities include runnable Go examples. That reduces integration surface; it does not eliminate the need to define recipient authorization, retention, password delivery, or an SLO.

The limitation is equally clear: Infrai is not a fit when policy requires PDF processing to remain inside infrastructure you operate, or when the organization has already standardized the entire control trail on one cloud and another API boundary would only add work. Choose Gotenberg or a local PDF tool for the first case. Choose the native S3, Cloud Storage, or Azure path for the second. DocRaptor, PDFMonkey, and PDFShift deserve evaluation when high-fidelity generation from HTML or templates is the main job, but generation alone does not settle encryption, signed-link revocation, or audit retention.

Self-hosted tools keep document bytes inside your own boundary, while object storage supplies signed delivery. The cost is yours to carry through patching, compatibility testing, capacity planning, key handling, and on-call ownership. Managed services move some of that load outward but add provider policy semantics and lock-in. I would reject any buy-versus-build review that counts request charges yet assigns zero cost to verifier testing and incident response; the better table has columns for conversion fidelity, key custody, revocation semantics, evidence export, peak monthly throughput, recovery testing, and after-hours ownership.

Set the alert threshold from the harm

A page should mean an engineer can stop live exposure: an unauthorized recipient still has an unexpired, unrevoked delivery credential. A link due to expire soon, a rejected download, or an old audit record missing optional enrichment is not the same class of event. Route those elsewhere.

Denied is healthy.

The false-positive cost is concrete. If every expired-link probe pages, on-call learns that access denial is an incident even though denial is the control working correctly. If the threshold waits for a successful unauthorized download, the alert arrives after the only preventive action has passed. Trigger on the intersection of invalid authorization and usable credential, enrich it with download evidence, and test revocation as part of the monthly report runbook.

Capacity planning belongs here too. Size the evaluator for issuance and authorization-change events, not just successful downloads; a portfolio-wide role change can create a sharp reconciliation burst. Define backlog age as a control SLI. Once that queue exceeds the promised revocation interval, the access SLO is already at risk even if every storage request returns success.

The final decision is uncomplicated: signed links govern the door, encryption protects the copy, signatures support integrity evidence, and audit events connect those controls to a human authorization decision. External delivery needs all four.

Further reading

Top comments (0)