Security model
Sigil’s security boundary is the Entra token, verified on every request. There is no network gate in front of it, and nothing anybody signs in with is a Sigil credential.
The one credential Sigil issues to your organisation is an API key, which an administrator creates deliberately for a script. It is described below, and no part of the product depends on one existing.
Why the add-in holds no secret
Section titled “Why the add-in holds no secret”The add-in’s JavaScript is served from a public URL. Anything embedded in it would be public too.
If the signature endpoint accepted a secret baked into that bundle, anyone who
viewed source could call it, and the email parameter would let them enumerate
every user’s name, job title and mobile number.
So the endpoint requires a verified user token instead. That is the whole design: the security comes from the token rather than from anything hidden.
What is verified
Section titled “What is verified”Every call to the API carries an Entra access token, and the server checks it before doing anything:
The tenant claim (tid), which both resolves the request to a tenant and selects
what the rest of the checks are made against.
The cryptographic signature, against the signing keys that tenant publishes.
The issuer, pinned to that tenant’s own Entra v2 issuer. A token whose issuer and tenant claim disagree is rejected, so naming a different tenant does not let a token be verified against keys it was not signed with.
The audience, pinned to Sigil’s application id. This one is fixed rather than per-tenant: every token for this API, from any organisation, carries it.
The delegated scope. The add-in’s token carries access_as_user and a browser
sign-in carries portal_admin, which is what separates the two. An add-in token
cannot reach an admin endpoint.
portal_admin is not itself a grant of administrative access, despite the name.
It is also what a colleague holds when they sign in to
fill in their own details, and it authorises nothing on
its own. Capabilities come from the roles held in Sigil, so a token carrying this
scope with no role behind it reaches that person’s own profile and nothing
further. See permissions.
Authentication paths
Section titled “Authentication paths”Add-in users authenticate through MSAL nested app authentication, brokered by
Outlook, using the /organizations authority so each person’s token is issued by
their own tenant.
Portal administrators authenticate through an MSAL single-page-app sign-in.
Neither involves a Sigil password. There is no password for Sigil to store, and no sign-in of any kind that does not go through Entra.
API keys
Section titled “API keys”An API key is the exception to everything above, and is scoped tightly enough to stay one.
It is created by an Admin, or by an Owner or Admin at a managed service provider looking after the organisation. It belongs to one organisation, and carries a set of capabilities but no role.
What it may reach is an allow-list rather than a set of blocked areas. One list names every operation a key can call, and a request outside it is refused before the route runs. The reason for that direction is what happens to an endpoint added later: under a list of blocked areas it would be reachable by every key issued so far, because nobody thought to block it. A build check fails on any endpoint that is on neither list, so adding one forces the decision.
The exclusions have a shape. A key is refused anything that acts outside Sigil, meaning sending mail, moving money in Stripe or granting portal access; anything that overrides a control the organisation set, such as switching publish approval off or moving work through the approval queue; and anything that reads a named individual’s directory record on demand. That last one covers the portal’s own per-mailbox download and preview, rule simulation and the directory picker, each of which answers “tell me about this mailbox” for an address the caller supplies. Aggregate reporting that happens to carry mailbox addresses is not refused, since that is most of what the credential exists for.
The add-in’s signature endpoints are refused too, because those take a per-mailbox token rather than an organisation-wide one, as are the key management routes and the partner routes.
The portal renders the list as a reference beside the keys, generated from the list that enforces it, so what a key is documented to reach and what it can reach are the same statement. Grant a key the narrowest set of areas its job needs, and see API keys.
The secret is returned once at creation and stored only as a SHA-256 hash, so a copy of the database yields no working key. A fast hash is the right choice here rather than the slow one a password would need: the value is 256 bits of random data rather than something a person chose, so there is nothing to guess.
Revocation is immediate and the row survives it, which keeps the change log’s attribution resolvable. Expiry is optional and checked on every request. Each key is rate limited to 600 requests a minute, because the service is shared across organisations and a runaway script should not become everybody’s problem.
Authorisation
Section titled “Authorisation”Authentication and authorisation are separate.
The token establishes who somebody is and which organisation they belong to. Their role, stored per tenant inside Sigil, decides what they can do. That is one of the six built-in roles, or a custom role an Admin has put together from the same capabilities. Admin authority itself cannot be granted through a custom role. Every server route declares the capability it requires and checks it independently.
The portal hides navigation a role cannot reach, but that is presentation. The server is the gate. See users and roles.
Directory access is read-only
Section titled “Directory access is read-only”Sigil requests three Microsoft Graph application permissions, all read-only:
User.Read.All, Organization.Read.All and GroupMember.Read.All.
No write permission is requested anywhere, so Sigil cannot modify your directory. See permissions for what each one is used for.
Why the signature endpoint accepts any mailbox in the tenant
Section titled “Why the signature endpoint accepts any mailbox in the tenant”The email parameter can name any mailbox in the calling tenant, not only the
caller’s own. This is deliberate: it is what allows a signature to be rendered for
a shared mailbox or an alias when somebody switches sending account.
The attributes returned are already visible to every colleague in the address book. What the token requirement adds is that they are not visible to anybody outside the organisation.
A sending address is verified against the mailbox’s own proxy addresses before it is printed into a signature. An unrecognised address falls back to the primary and cannot be injected.
Tenant isolation
Section titled “Tenant isolation”Each organisation’s data is kept in a database of its own. A new organisation is created with one from the start, and every organisation that joined before that was moved across on 28 September 2026. Its templates, rules, users, telemetry and change log are physically apart from everybody else’s, so keeping them apart does not depend on every query remembering to filter by organisation. The copy each organisation’s data was moved from is still held in the shared database for now, as a way back if a move had to be reversed, and is due to be removed.
The tenant id is still on every row, and every storage function still takes it, so a query cannot forget to scope itself even inside an organisation’s own database. That is the check the shared database relied on before the move, and it was kept rather than dropped.
A few records stay in the shared database for every organisation, because Sigil has to find them before it knows which organisation a request belongs to: the organisation record itself, billing and subscription state, API key hashes, tracked link destinations, portal sign-in records, the names shown against the people who use the portal, and partner records. None of them is content you authored.
Object storage keys are prefixed by tenant. Cache keys are tenant-salted. Tracked link slugs are derived from the tenant id and the destination, so two organisations pointing at the same URL get distinct slugs and distinct counts.
The signature endpoint resolves the tenant from the token and refuses an unknown or suspended one.
Rendering safety
Section titled “Rendering safety”Directory values are HTML-escaped when they are substituted into a template, so a value containing markup cannot break the signature or inject anything.
Any placeholder left unresolved is stripped, so a literal {{jobTitle}} can never
reach a recipient.
Profile fields are checked a second time, on the way
in. They are the only values a signature carries that a colleague typed rather
than an administrator configured or the directory supplied, and escaping alone
does not cover every case: a link target that is entirely one placeholder cannot
be checked at render time, because at that point the value is not yet known. So a
field declared as a web address accepts only ordinary http and https links,
and refuses anything else when it is saved. The same check applies when an
administrator edits somebody else’s values, since it protects the recipient
rather than policing the author.
Footer markup is cleaned on the way in for a related reason. A footer is rich text an administrator writes, it is stored as written, and it is loaded back into an editor when a colleague opens it, so script-capable markup in a footer would run in that colleague’s session rather than in a recipient’s mail client. Because a role can edit footers without reaching users, settings or billing (Compliance, an MSP Technician, or a custom role given footers alone), leaving it uncleaned would turn the narrowest roles in the product into a route to the widest. Scripts, frames, forms, embedded objects, event handler attributes and executable URL schemes are removed when the footer is saved, and the markup is parsed by a real HTML parser rather than matched with a pattern. The editor cleans stored markup again before showing it, so a footer saved before the rule existed is covered too.
Browser response headers
Section titled “Browser response headers”Sigil’s responses carry a baseline set of browser security headers, and the set covers the portal’s own pages as well as the API. That distinction is the one worth checking in any product of this shape: the pages an administrator actually browses are served differently from the API they call, so a header applied to only one of them protects the half nobody was attacking.
| Header | What it does |
|---|---|
Strict-Transport-Security |
Six months, including subdomains. A browser that has loaded usesigil.app once will not try plain HTTP for it again |
X-Content-Type-Options |
Stops a browser guessing a response is a different type from the one it was sent as |
X-Frame-Options and a frame-ancestors policy |
The portal cannot be framed by another site. Both are sent, because browsers do not all honour the same one |
Content-Security-Policy |
Limits where the portal’s pages may load code and send data from. See below |
Referrer-Policy |
A tracked link’s destination sees that the click came from Sigil, never the full path of the redirector |
HSTS preloading is deliberately left off. Preloading needs a year-long maximum age, and browsers honour a preload entry long after the header itself is withdrawn, which makes it far harder to undo than to switch on.
Two headers are switched off on purpose rather than by omission, because their usual settings break something people rely on. Isolating the browsing context would sever the popup window that Entra re-authentication uses. Restricting cross-origin resource loads would put the add-in’s own call to the signature endpoint at risk, and that call is cross-origin by design. Both choices are held in place by tests, so a library upgrade that quietly reinstates a default fails a build rather than somebody’s Outlook.
The portal’s pages are served with an enforced content security policy, in place since 10 September 2026. It is there for the day sanitising misses something: templates, footers and the designer all carry markup that administrators write, and the policy is the layer that holds if any of it slips through.
| Directive | What it allows |
|---|---|
| Scripts | Sigil’s own origin, plus Stripe’s card form library from js.stripe.com. No inline script and no script from a content delivery network |
| Styles | Sigil’s own origin, plus inline styles, which the portal and signature previews rely on |
| Fonts | Sigil’s own origin. The portal’s fonts are hosted by Sigil, so no request goes to a font service |
| Connections | Sigil’s own origin, Microsoft’s sign-in service login.microsoftonline.com, and api.stripe.com, where the card form sends what is typed into it |
| Frames | Sigil’s own origin, Microsoft’s sign-in service, and Stripe’s js.stripe.com and hooks.stripe.com, which draw the card form and a bank’s card check |
| Form posts | Sigil’s own origin and Microsoft’s sign-in service |
<base> and <object> |
Nothing at all |
Stripe’s script is the one third-party script the portal loads, and the reason
it is an exception is Stripe’s rule rather than Sigil’s: it must come from
Stripe’s own address and may not be bundled or copied, because that is what
keeps card entry with Stripe rather than with Sigil. Every field of the card form
is a frame from Stripe, so the portal never sees a card number. Only the pages
that offer the card form load it: Billing, Partner billing and Getting started.
No other page carries a third-party script.
Stripe’s list also suggests allowing every address under js.stripe.com; the
policy names the exact address instead.
A violation is reported back to Sigil rather than only being blocked, so a page that breaks under the policy is noticed rather than left for a customer to find. A stricter candidate runs alongside it in report-only mode, identical but for images, to find out whether any template still points at an image host that is not Sigil’s own. API responses carry only the framing directive, since the rest of the policy only means something on a page.
The add-in’s policy
Section titled “The add-in’s policy”The add-in has two pages of its own: the “My signature” pane, and the hidden page Outlook runs to add a signature automatically when someone starts an email. Both have enforced a full content security policy since 20 October 2026. Before that the policy ran for six weeks in report-only mode, from 7 September, while it was exercised from Outlook on the web, Windows, Mac, iOS and Android. Report-only blocks nothing and only says what it would have blocked, which is how a policy gets proven inside other people’s Outlook before it can break a signature.
| Directive | What it allows |
|---|---|
| Scripts | Sigil’s own origin, Microsoft’s Office.js host appsforoffice.microsoft.com, and ajax.aspnetcdn.com, which the Office.js runtime fetches part of itself from |
| Styles | Sigil’s own origin, plus inline styles, which the signature preview relies on |
| Images | Sigil’s own origin, images embedded in the signature, and any https address, so a template that points at an image on your own website still previews in the pane |
| Fonts | Sigil’s own origin |
| Connections | Sigil’s signature service at portal.usesigil.app, and Microsoft’s sign-in service |
| Frames | Sigil’s own origin, Microsoft’s sign-in service, and a Microsoft telemetry host described below |
| Form posts | Sigil’s own origin and Microsoft’s sign-in service |
| Embedding | Outlook on the web and the other Microsoft 365 hosts, which is where Outlook runs both pages |
<base> and <object> |
Nothing at all |
The policy allows unsafe-eval for scripts, which the portal’s policy does not.
That is Microsoft’s requirement rather than a gap: the Office.js runtime builds
code from strings as it starts, and the report-only weeks showed it doing so on
both pages. Blocking it would stop the add-in loading. The same weeks turned up
the other two Microsoft entries. One is the second script host. The other is
telemetryservice.firstpartyapps.oaspapps.com, a hidden frame Office.js opens to
send Microsoft its own usage telemetry. Nothing of Sigil’s runs in that frame,
and a framed page cannot read the pane around it.
Violations on either page are still reported to Sigil. On an enforced policy a report means something was blocked in somebody’s Outlook, and that would otherwise show only in a browser console nobody has open. A stricter candidate runs alongside in report-only mode, the same policy with images limited to Sigil and embedded images, to learn whether any real template points at an outside image host.
The link domain
Section titled “The link domain”Tracked links are served from e-clk.usesigil.app, which answers /r/ redirects
and contact card downloads and returns 404 for every
other path. The one addition is /.well-known/, where a certificate authority
checks domain ownership before issuing a certificate.
Recipients click those links, so the domain is the most widely exposed surface Sigil has. Keeping the portal and API off it means the exposed surface is a redirect and nothing else.
An organisation can serve its links from a hostname of its own instead, with the branded link domain add-on. A branded hostname is not closed down path by path in the same way, because telling a customer’s hostname apart from Sigil’s own would mean a database lookup in front of every portal and API request. The portal and API still require sign-in there like anywhere else. The protection that matters is on the two paths that need no sign-in, redirects and contact cards, and it is described next.
A branded hostname resolves only the links belonging to the organisation that claimed it. A slug belonging to another organisation returns 404 on it, even though the same slug resolves on the shared hostname. Without that scoping, one customer’s hostname would serve another customer’s links and record the click against them, on a path that is unauthenticated by necessity.
A hostname an organisation has ever claimed answers for that organisation or for nobody. It is never treated as unclaimed while the record naming it exists, since an unclaimed hostname is one that resolves everybody’s links.
Operator access
Section titled “Operator access”Tophhie Cloud’s own support staff can view a tenant read-only through an impersonation session that expires on the server after 30 minutes. Destructive operator actions require a fresh interactive re-authentication in the moment, and every operator action is written to an audit log retained indefinitely.
Anything done to your organisation is also written to your own change log, badged as support. The read-only session above is one of the things recorded there, so being viewed is visible to you and not only to Tophhie Cloud.
See compliance.
