<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Transmit Security Engineering - Medium]]></title>
        <description><![CDATA[Digital identity is evolving at a breakneck pace. Businesses need a CIAM platform that delivers leading-edge innovation before the latest standards, technologies and cyberthreats surpass their defenses. Transmit Security powers you with CIAM platform born in the cloud. - Medium]]></description>
        <link>https://medium.com/transmit-security-engineering?source=rss----262939e6cd0e---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Transmit Security Engineering - Medium</title>
            <link>https://medium.com/transmit-security-engineering?source=rss----262939e6cd0e---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 10:06:59 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/transmit-security-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Not All Faces Are Matched Equally: Engineering Face Recognition Across Races and Nations]]></title>
            <link>https://medium.com/transmit-security-engineering/not-all-faces-are-matched-equally-engineering-face-recognition-across-races-and-nations-be753fdb1c6f?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/be753fdb1c6f</guid>
            <category><![CDATA[ai-fairness]]></category>
            <category><![CDATA[face-recognition]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[identity-verification]]></category>
            <category><![CDATA[computer-vision]]></category>
            <dc:creator><![CDATA[Dvir Dadon]]></dc:creator>
            <pubDate>Mon, 19 Oct 2026 13:59:27 GMT</pubDate>
            <atom:updated>2026-10-19T13:59:25.915Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3i4VqPgqRY_XD5Kr_R-3Gg.png" /></figure><p><em>How we test and tune face matching for identity verification across races and nations</em></p><p>This is the third post in our identity verification (IDV) engineering series. In <a href="https://medium.com/transmit-security-engineering/unveiling-face-recognition-from-pixels-to-identity-11c800fdd279">Unveiling Face Recognition: From Pixels to Identity</a> we walked through how a face-recognition pipeline works, and in <a href="https://medium.com/p/ce3a335029bb">Inside Auto Capture</a> we showed how we capture good document and selfie images on the device. In this post we share our findings around model bias: where it enters a face-matching pipeline, how we measure it across races and nations, what we do when we find a real gap, and what we found when we looked.</p><h3>The one-second decision</h3><p>Someone opens a bank’s onboarding flow, photographs their ID card, then takes a selfie. About a second later, our IDV service answers one question: is the face on the document the same face in front of the camera?</p><p>That answer is a number. Two face images become two vectors, the distance between them is measured, and the distance is compared against a threshold. Everything that shaped those vectors, above all who was in the training data, shapes who gets waved through and who gets told to try again.</p><p>Face recognition is not equally accurate for everyone. In 2019, the U.S. National Institute of Standards and Technology (NIST) evaluated 189 algorithms for demographic effects and found that, for many of them, false-match rates differed by a factor of 10 to 100 between demographic groups. A year earlier, the Gender Shades study showed commercial gender classifiers erring on up to 34.7% of darker-skinned women versus under 1% of lighter-skinned men.</p><p>Those studies cover general-purpose face recognition. Identity verification adds twists of its own. In this post you’ll see where bias enters a face-matching pipeline, why document-based verification makes it harder, and two things we actually did about it: we built a benchmark that could see bias and changed our embedding model because it found a real gap, and we evaluated our models on a population under-represented in our training data, only to find that the bias we feared was not the one in the data.</p><h3>Where the pipeline can go wrong</h3><p>A face-matching system is a four-stage pipeline. Each stage can introduce demographic error, and errors compound.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s7sT891O_5of5RgLvsPN3A.png" /></figure><p><strong>1. Detect.</strong> A detector finds the face and five landmarks: both eyes, the nose tip, and the mouth corners. Detectors trained mostly on well-lit, high-contrast faces lose recall on darker skin in dim lighting and on tiny, low-contrast document portraits. A missed detection is a rejection before any matching has happened.</p><p><strong>2. Align.</strong> The five landmarks are warped onto canonical positions in a fixed-size crop, typically 112 by 112 pixels. Landmark errors propagate downstream, and eye-landmark errors are systematically larger for some eye shapes.</p><p><strong>3. Represent.</strong> A convolutional network maps the aligned crop to a unit-length vector, usually 512 numbers, called an embedding. Modern networks are trained with margin-based softmax losses on millions of identities. This is where most of the bias lives, and the mechanism is simple: the network learns to separate the identities it was shown. If most of those identities share a demographic, the embedding space is finely partitioned there and coarsely partitioned elsewhere. Faces from under-represented groups land closer together, so different people from that group look more alike to the model.</p><p><strong>4. Match.</strong> The two embeddings are compared with cosine distance and the result is checked against one global threshold. One threshold, many impostor distributions. When a group’s impostor distances sit closer to zero, the same threshold accepts more impostors from that group. A single accuracy number hides this completely. You have to report the false match rate (FMR) and false non-match rate (FNMR) per group.</p><p>A short history of embedding methods explains why stage 3 dominates. Eigenfaces (1991), the classical approach, represented a face as weights over principal components, optimized for reconstruction rather than discrimination. FaceNet (2015), the first widely adopted deep-learning approach, trained a network with a triplet loss so that same-person embeddings are pulled together and different-person embeddings pushed apart. ArcFace (2018), the loss function behind most of today’s face-recognition networks, replaced triplets with an angular margin on a classification loss, forcing tighter identity clusters on a hypersphere. AdaFace (2022), a refinement of ArcFace, made that margin adapt to image quality, so blurry or low-resolution faces get a gentler push. Each step made embeddings more discriminative. None of them made embeddings fair by itself, because the loss knows nothing about demographics. It only knows the identities it is shown.</p><h3>Identity verification is a harder version of the problem</h3><p>Most face-recognition benchmarks compare two photographs of similar quality. Document verification does not. One side is a live selfie from a phone camera. The other is a portrait printed on a document: taken years earlier, heavily compressed, sometimes grayscale and laser-engraved into polycarbonate, sometimes sitting under a hologram, and often only a few hundred pixels wide once cropped.</p><p>This is where “nations” enters the picture as an engineering variable, not only a demographic one. A laminated paper card from one country looks nothing like a polycarbonate card from another, and both look nothing like a passport data page. Our original embedding model was chosen for robustness to age gaps between the document portrait and the selfie. The portrait side of the pair varies by issuing country at least as much as faces vary by ancestry, and a matcher that was never evaluated on that variety will surprise you in production.</p><h3>Case study 1: a benchmark that could see bias</h3><p>In early 2025 we committed to NIST-style fairness targets for our 1:1 matcher: FMR and FNMR gaps between the best- and worst-performing demographic groups within a fraction of a percentage point. The first problem was not the model. It was that our benchmark could not measure the target. It covered a few hundred identities from one country, produced one accuracy number, and took two and a half hours per run.</p><p>Here is what we changed.</p><p><strong>We built a demographically labeled evaluation set from public data.</strong> We combined VGGFace2, the class-balanced BUPT-CBFace set, and synthetic celebrity-style faces, then labeled every identity into one of six groups with an off-the-shelf demographic classifier. To reduce label noise we ran the classifier on every image of a person and took a majority vote. Automatic labels are imperfect. They are good enough to find gaps between groups, and not good enough to make claims about any individual.</p><p><strong>We sampled hard impostors.</strong> Impostor pairs (two different people) were drawn from within the same demographic group. Cross-group impostors are easy and inflate results; the NIST differentials are within-group differentials.</p><p><strong>We computed embeddings once.</strong> Each face is embedded a single time and pairs are compared from cache. The run dropped from two and a half hours to under a minute, which turned “one experiment a day” into “twenty experiments a day”.</p><p><strong>We logged every failure as an image pair.</strong> This exposed duplicate identities filed under different IDs in the “different person” set. Part of our measured error was benchmark noise, not model error.</p><p>With the benchmark fixed, the baseline model showed a real gap. FNMR is the share of genuine pairs (same person) that the matcher wrongly rejected; FMR is the share of impostor pairs (different people) that it wrongly accepted. On roughly 480 identities per group, at a fixed threshold:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LzlNLzvU7BhWqXBV_KIPwg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ai3Y_yyK7_vcC7sz3Zs7nA.png" /></figure><p>Two caveats. At a few hundred identities per group, a difference of about one percentage point is inside the noise. And measuring FMR at NIST’s operating points, one in a thousand or lower, needs orders of magnitude more impostor pairs than this. The table finds a direction, not a verdict.</p><p><strong>What did not work.</strong> Test-time augmentation (random crops, flips, color jitter, averaged over ten passes) made results slightly worse. Swapping in other public models such as VGG-Face and GhostFaceNet was worse still. Majority voting across several models gave no gain.</p><p><strong>What worked.</strong> Two newer open models, AdaFace trained on the WebFace12M dataset and ElasticFace, each beat our original model on the diverse set. The best result came from fusing: concatenate the L2-normalized embeddings of three backbones (our original model, AdaFace, and an ArcFace-style iResNet-100) into a single 1,536-dimensional vector, then compare with cosine distance as before. On a balanced set of 200 identities per group:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sjiLLw6I8i1sMDch2ihCWA.png" /></figure><p>The gains are small in absolute terms, which is what you expect from models that are already strong. What matters is that the impostor side improved: the fused model accepts far fewer same-group impostors, which is exactly the failure mode the NIST report flags.</p><p>The fusion itself is a few lines:</p><pre>import numpy as np</pre><pre>def l2_normalize(v):<br>    return v / np.linalg.norm(v)</pre><pre>def fused_embedding(face, backbones):<br>    &quot;&quot;&quot;Concatenate L2-normalized embeddings from several backbones.&quot;&quot;&quot;<br>    return np.concatenate([l2_normalize(m(face)) for m in backbones])</pre><pre>def cosine_distance(a, b):<br>    return 1.0 - float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))</pre><pre>def match_confidence(distance):<br>    &quot;&quot;&quot;Map a cosine distance to an angular score in [0, 1].&quot;&quot;&quot;<br>    return 1.0 - np.arccos(np.clip(1.0 - distance, -1.0, 1.0)) / np.pi</pre><p>Fusion also solved a production problem for free. The first 512 numbers of the fused vector are exactly the old model’s vector, so every embedding already stored for fraud-face galleries and repeat-face search stayed valid. We migrated online: new sessions write the 1,536-dimensional vector, searches against older records read the 512-dimensional prefix, and nothing went down while the galleries were re-encoded in the background.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*phqJGxTyULtmIQDV_jnjdA.png" /></figure><p>And the per-group report that gates every model change since is just as short:</p><pre>from collections import defaultdict</pre><pre>def per_group_rates(pairs, threshold):<br>    &quot;&quot;&quot;pairs: iterable of (group, distance, is_same_person).<br>    Impostor pairs must be sampled *within* the same group.&quot;&quot;&quot;<br>    s = defaultdict(lambda: {&quot;fm&quot;: 0, &quot;imp&quot;: 0, &quot;fnm&quot;: 0, &quot;gen&quot;: 0})<br>    for group, dist, same in pairs:<br>        if same:<br>            s[group][&quot;gen&quot;] += 1<br>            s[group][&quot;fnm&quot;] += dist &gt;= threshold   # genuine pair rejected<br>        else:<br>            s[group][&quot;imp&quot;] += 1<br>            s[group][&quot;fm&quot;] += dist &lt; threshold     # impostor accepted<br>    return {g: {&quot;FMR&quot;: v[&quot;fm&quot;] / v[&quot;imp&quot;], &quot;FNMR&quot;: v[&quot;fnm&quot;] / v[&quot;gen&quot;]}<br>            for g, v in s.items()}</pre><pre>def fairness_gap(rates, metric):<br>    values = [r[metric] for r in rates.values()]<br>    return max(values) - min(values)</pre><h3>Case study 2: the bias we expected was not the one we found</h3><p>While evaluating our models on a population with different demographic characteristics than our training data, we had one specific worry. Before matching, the selfie passes a set of quality checks, one of which rejects images where the eyes appear closed. Eyes-closed detectors are a textbook demographic failure mode: a model trained mostly on one eye shape fires falsely on another, and every false trigger is a “please retake your selfie” loop for a legitimate user.</p><p>We set the targets before touching any model. For presentation-attack detection we adopted the ISO/IEC 30107–3 metrics (attack presentation classification error rate, bona fide presentation classification error rate, and their average), each capped at 5%. For matching we set FNMR at an FMR of 0.1% to at most 1%, in the NIST 1:1 style. And for parity we required no sub-population to trail the global baseline by more than two percentage points on any of those numbers.</p><p>Then we measured on two fronts. We built a labeled evaluation set with sub-populations tagged. And, before waiting for it, we audited production: using the issuing country of the verified document as a coarse proxy for population, we computed eyes-closed and liveness rejection rates for users from that population and for everyone else within the same tenants, with confidence intervals, and reviewed samples by hand.</p><p>The result surprised us. Users from the population we were worried about did not have an elevated eyes-closed rejection rate. In the largest tenants the opposite held: everyone else was rejected for closed eyes several times more often, with tight confidence intervals and enough volume that this was not noise. Manual review agreed. We shipped no retraining, and the evaluation set and harness became a permanent monitoring asset.</p><p>Three lessons came out of that exercise.</p><ul><li>“This model is biased against group X” is a hypothesis. It is a very plausible one, and you should still test it before you spend a quarter fixing it.</li><li>The check was cheap because the machinery from case study 1 already existed. Fairness testing is a fixed cost that pays back every time a new population comes up.</li><li>The problem we did find was not a demographic one. The check fires more often than it should for everyone, which is a general accuracy question with a product-defined target, not a fairness question, and the target was what we were missing. It went to the product backlog as an accuracy item, not to the fairness ladder below.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AZG3u1xRK5mL9-EO4Op9tQ.png" /></figure><h3>What we do when a gap is real</h3><p>Measuring is half the job. When the per-group report shows a real gap, we work through a fixed ladder, cheapest rung first, and re-run the report after each rung on both the affected group and the global baseline. A fix that helps one group and hurts another does not ship.</p><ol><li><strong>Recalibrate the threshold.</strong> Per-group ROC curves often show that one global threshold sits at a bad operating point for one group. Moving it, or giving a quality check its own threshold, is a configuration change that takes a day.</li><li><strong>Change the model, not the data.</strong> Swap in, or fuse with, a backbone trained on a more diverse corpus. This is what closed the gap in case study 1: no training on our side, a few lines of code, and a measurable drop in same-group false matches.</li><li><strong>Fine-tune on the under-represented population.</strong> Add a labeled split for that group to the training data and fine-tune with class-balanced sampling or a focal loss, so the minority group is not drowned out by the majority. This needs consented data, so we source it from a data partner or, for attack coverage in liveness, generate it synthetically from genuine samples.</li><li><strong>Replace the model.</strong> Some checks ship as inference-only artifacts with no training pipeline behind them. If recalibration does not close the gap, the honest fix is a retrainable architecture, and that is a quarter of work, not a sprint.</li></ol><p>Every rung ends the same way: the per-group report on the affected population and on the global baseline, side by side. Case study 1 stopped at rung two. Case study 2 never needed a rung at all, which is the point of measuring first.</p><h3>What we keep doing</h3><p>The two case studies turned into habits. If you run a face matcher in production, these are the ones worth copying.</p><ul><li><strong>Report FMR and FNMR per group, never just accuracy</strong>, and track the gap between the best and worst group as a first-class metric.</li><li><strong>Sample impostors within a group.</strong> Cross-group impostors are the easy case and hide the failure NIST measures.</li><li><strong>Use the document’s issuing country as a production proxy label</strong>, with confidence intervals. It is coarse, but it needs no new data collection and catches drift early.</li><li><strong>Treat the threshold as a product decision.</strong> Look at the ROC curve per group before choosing it, because one threshold means different operating points for different groups.</li><li><strong>Benchmark the document side.</strong> Countries differ in document manufacturing more than populations differ in faces. A portrait engraved in grayscale on polycarbonate is a different input than a color photo on paper.</li><li><strong>Gate every model change on the per-group report.</strong> A model that improves the average and widens the gap does not ship.</li></ul><h3>The bigger picture</h3><p>Fairness in face recognition is not a dataset you buy once. It is a measurement you build, and then keep running as models change and populations grow. The fused-embedding change in case study 1 was a modest model improvement made possible by a large benchmarking improvement. The evaluation in case study 2 found no model problem at all, and was still worth doing, because now we know rather than assume.</p><p>As identity verification becomes the front door to banking, telecom, and government services across the world, “works for most people” is not a shipping criterion. The engineering to get past it is not exotic. It is labeled evaluation sets, honest per-group metrics, hard impostors, a fixed ladder of fixes, and the discipline to look before you fix.</p><p><strong>References</strong></p><ul><li>Grother, Ngan, Hanaoka. <em>Face Recognition Vendor Test Part 3: Demographic Effects.</em> NISTIR 8280, 2019.</li><li>Buolamwini, Gebru. <em>Gender Shades.</em> FAT* 2018.</li><li>Deng et al. <em>ArcFace: Additive Angular Margin Loss for Deep Face Recognition.</em> CVPR 2019.</li><li>Kim, Jain, Liu. <em>AdaFace: Quality Adaptive Margin for Face Recognition.</em> CVPR 2022.</li><li>Boutros et al. <em>ElasticFace: Elastic Margin Loss for Deep Face Recognition.</em> CVPRW 2022.</li><li>Wang, Deng. <em>Mitigating Bias in Face Recognition Using Skewness-Aware Reinforcement Learning.</em> CVPR 2020 (BUPT balanced datasets).</li><li>ISO/IEC 30107–3, <em>Biometric presentation attack detection, Part 3: Testing and reporting.</em></li></ul><p><em>Dvir Dadon leads the IDV Vision team at Transmit Security, the computer-vision and machine-learning group behind document and face verification in the Mosaic identity platform. His team owns the models, benchmarks, and fairness testing described in this post.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=be753fdb1c6f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/not-all-faces-are-matched-equally-engineering-face-recognition-across-races-and-nations-be753fdb1c6f">Not All Faces Are Matched Equally: Engineering Face Recognition Across Races and Nations</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The ORM Trap: How We Started Catching Database Problems Before Production]]></title>
            <link>https://medium.com/transmit-security-engineering/the-orm-trap-how-we-started-catching-database-problems-before-production-35374ea07fff?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/35374ea07fff</guid>
            <category><![CDATA[database-performance]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[performance-testing]]></category>
            <category><![CDATA[typeorm]]></category>
            <dc:creator><![CDATA[Idan Berman]]></dc:creator>
            <pubDate>Thu, 17 Sep 2026 07:35:56 GMT</pubDate>
            <atom:updated>2026-09-17T07:35:55.485Z</atom:updated>
            <content:encoded><![CDATA[<h4>You could have a well-tested, well-structured codebase (real integration tests, a normalized schema, indexes on every table) and still have no idea whether your queries will hold up when the database grows. No warnings. No errors. The ORM runs the query, the tests pass, and somewhere in production the planner quietly reads the entire table to answer something that should have used an index. We built a tool that catches this automatically: in the test suite, on every pull request, before code reaches production.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*94wbVFqI_l8z2qgAazOlWA.jpeg" /></figure><h3>The Magic, and What It Costs</h3><p>ORMs sell a compelling promise: write TypeScript, get a database. Declare your entities, define your relations, call find() with a filter. The ORM handles the rest. You never have to think about SQL.</p><p>That magic is real. But it comes with a cost: your entity and relation declarations deliberately say nothing about index shape or join strategy. They shouldn’t: that’s not what they’re for. The problem is that nothing else tells you either. Most of the time it doesn’t matter. Occasionally it matters enormously, and the ORM gives you no signal either way.</p><p>The harder class of problems is those that don’t crash, don’t return wrong data, and don’t show up in your logs until there’s enough data behind them for the cost to become visible. A query that was fast with thousands of rows becomes slow enough to page someone with millions of rows — same code, same tests, different outcome.</p><h3>The Trap</h3><p>The ORM generates SQL you never wrote and never reviewed, and it fails in ways that neither CI nor lower environments will surface until scale exposes them:</p><ul><li>Queries that drop tenant scope entirely: a bare WHERE id = :id, or an UPDATE/DELETE, on a tenant-scoped table with no tenantId anywhere in it, no join involved at all.</li><li>Queries badly shaped from the start, unreviewed because the ORM made SQL invisible.</li><li>Search endpoints doing full-table scans because no index was ever considered.</li><li>Cascading relation loads that load every row in a relation; trivial with ten records, a full-table read with millions.</li><li>Delete cascades that fire one query per child row: harmless at small scale, a query storm in production.</li><li>Implicit SELECT DISTINCT injected when combining relations with pagination, forcing a full deduplication pass across the entire result set before LIMIT even runs.</li></ul><p>The common thread across all of them: no systematic way to catch any of it before it reached production.</p><p>The most structural of these, and the most instructive, is a join between tenant-scoped tables that quietly drops tenantId from the ON clause. It&#39;s worth walking through in detail, because it explains why indexes stop being usable at all, not just used inefficiently.</p><h3>The Join That Makes Indexes Unreachable</h3><p>Let’s take, for example, a simple join between two tenant-scoped tables. It’s the kind the ORM generates automatically when you load a relation.</p><p>Our system is multi-tenant. Every row in every table carries a tenantId column: top-level records, child tables, junction tables, every relation. Tenant segregation is a correctness requirement, not a filter. Because every query filters by tenant, every index leads with tenantId: composite indexes (tenantId, …), primary keys (tenantId, id), and FK relationships structured as (tenantId, userId).</p><p>The reason this matters structurally: composite indexes are only usable when the query’s filter covers a leftmost prefix of their column order. Skip the leading column, and that index has no usable entry point. In a schema like ours, where every index on every tenant-scoped table is deliberately built to lead with tenantId, that means there&#39;s nothing else for the planner to reach for either, and it falls back to a full table scan.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*onrAkNr4hffhprgfsK0sww.jpeg" /></figure><p>When our ORM joined two tenant-scoped tables, it joined on whatever its @JoinColumn metadata declared, just the structural key in this case:</p><pre>@JoinColumn([{ name: &#39;userId&#39;, referencedColumnName: &#39;userId&#39; }])</pre><p>That declaration is what generates the ON clause for every find() call and every relation-based createQueryBuilder join alike: both resolve the join from the same entity metadata, so both inherited the same gap. tenantId was absent from the ON clause entirely, which meant the child table&#39;s indexes (already correctly composite, already leading with tenantId) had no usable entry point. The leftmost-prefix rule above cuts both ways: it&#39;s exactly as strict against a well-formed join missing the leading column as it is against a malformed WHERE clause.</p><p>The fix is completing that declaration. Once it’s complete, nothing further is needed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PLuWSCEDjPh97KSb6r55qQ.jpeg" /></figure><p>The right-hand panel’s ON clause is doing the real work. c.tenantId = p.tenantId ties the child&#39;s tenant to the parent&#39;s, and WHERE p.tenantId = :tenantId (required on every query in this codebase) ties the parent&#39;s tenant to a literal. Postgres&#39;s planner combines the two on its own: if c.tenantId equals p.tenantId, and p.tenantId equals a specific value, then c.tenantId must equal that same value too; nobody has to write that third fact down for the planner to know it. This is called <strong>an equivalence class</strong>: a group of expressions the planner has proven are all equal to each other, so that pinning any one of them to a literal pins all of them. The planner then pushes this derived fact down as a real index condition on the child table.</p><p>In TypeORM, that’s a schema-level change: add tenantId to the same @JoinColumn array and every find() call, along with any createQueryBuilder join that references the declared relation, inherits it automatically.</p><pre>@JoinColumn([<br>  { name: &#39;tenantId&#39;, referencedColumnName: &#39;tenantId&#39; },<br>  { name: &#39;userId&#39;, referencedColumnName: &#39;userId&#39; },<br>])</pre><p>The ORM abstraction doesn’t disappear: entities, relations, and TypeScript types all stay.</p><h3>Why Your Tests Won’t Catch It — And Neither Will Your Other Environments</h3><p>We had tests. They passed. The query was fast. Nothing indicated a problem.</p><p>The join case above is closed: completing the @JoinColumn was the whole fix, and it holds at any scale. But it&#39;s worth understanding the general principle behind why a broken query can pass every check on the way to production in the first place, because it explains far more than just this one bug.</p><p>The problem starts with the data. Component tests run against a real PostgreSQL instance, but the data in that instance is whatever you inserted to set up the test: a handful of rows, sometimes just one. That’s not a dataset; it’s a scaffold. The CI seed data does better, a few thousand rows, but still orders of magnitude away from production. Whatever a query actually costs to run, indexed or not, depends entirely on how much data it has to read past, and that number is wildly different in each environment.</p><p>Here’s the part that actually explains why. When a query can’t use an index (because the index isn’t there, or because nothing in the query gives the planner a usable entry point into it), Postgres reads the table the only way left: row by row, in full. That’s not a rare fallback; it’s just what “no index” means in practice.</p><p>Reading a handful of rows this way costs nothing: a few microseconds, indistinguishable from an indexed lookup on any stopwatch a developer is actually watching. Reading millions of rows this way costs real, measurable time. The query is identically broken in both cases (same SQL, same missing index, same plan shape), but only one of them is slow enough to notice. A test environment seeded with three rows cannot tell “this query is well-indexed” apart from “this query happens to be reading a tiny table.” Both finish instantly, for entirely different reasons.</p><p>The planner does pick between several strategies for executing a join (Nested Loop, Hash Join, Merge Join) based on cost estimates that are themselves driven by these same row-count differences. But whichever strategy it settles on, a relation with no usable index still gets read in full. The strategy changes how the join is stitched together, not whether a missing index gets found.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mDt_NUv2Hjql-_Ghb9z6SA.jpeg" /></figure><p>Our environments sit at genuinely different scale tiers, and an unindexed query’s cost scales with exactly that:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xSs_wcg49tjm3_jLZbBDaw.jpeg" /></figure><p>A query can pass every test, look clean in staging, and run fine in several production environments, then turn into a visibly slow full table scan in the next one, once the table it’s reading unindexed finally holds enough rows to notice. Different customers use different features and accumulate different data volumes. One environment not surfacing a problem is no guarantee the next one won’t.</p><h3>Automating EXPLAIN</h3><p>Once we understood the problem, the fix for any individual query was clear. The process of finding them all was not. When a slow query surfaced in production, we’d pull it from logs, run EXPLAIN ANALYZE against the affected environment, read the plan, identify the issue, write a fix, and deploy. Then wait for the next one. Dozens of repositories, hundreds of query paths, multiple production environments each at a different scale tier. And every new feature added new ORM calls generating new SQL we hadn&#39;t reviewed. We needed something that ran automatically on every pull request, across every query in the codebase.</p><p>We already had the queries. Every repository had component tests running against a real PostgreSQL instance via Docker. Every query path covered by tests was executing in tests. We just weren’t looking at the plans.</p><p>We built a custom TypeORM logger that hooks into the ORM’s query pipeline. Every SQL statement gets captured: the actual raw SQL TypeORM generates, along with its bound parameters and a stack trace identifying which repository method issued it. No guessing what the ORM produces; we see it directly. At the end of the test suite, we run analysis over everything collected.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PznYPmgFv576PmUV4hDDFA.jpeg" /></figure><p><strong>Pass 1 — Normal EXPLAIN</strong>. Run EXPLAIN FORMAT JSON on every captured query, parse the plan tree, look for sequential scans on non-trivial tables and for index scans that read significantly more rows than they return. Any such finding fails the build.</p><p><strong>Pass 2 — EXPLAIN with Nested Loop disabled.</strong> Nested Loop has a property no other join strategy shares: because it iterates one specific outer row at a time, it can substitute that row’s actual runtime value into the inner lookup, even when nothing elsewhere in the query pins that value to a literal. Hash Join and Merge Join have no equivalent — they plan the inner side once, so a condition that’s only a correlated reference has nothing to attach to, and the inner table gets read in full. This matters because CI’s small scale would make the planner pick Nested Loop naturally anyway, hiding the fragility until production volume eventually forces a different strategy. So we run a second EXPLAIN withSET LOCAL enable_nestedloop = false, wrapped in a transaction that rolls back automatically — if index usage collapses the moment Nested Loop is off the table, that’s a real structural finding.</p><p>One caveat: for a single-row primary-key lookup, the signal is weaker, since Hash Join would never realistically get chosen there regardless of scale. The value concentrates on bulk and multi-row paths, where the outer side’s size depends on caller behavior rather than a fixed key.</p><p><strong>Pass 3 — SQL text analysis.</strong> Some tenant isolation problems are invisible to the query plan. Some of them aren’t performance problems at all. We scan the raw SQL of every captured query for two distinct failure classes. First, and most severe: any query against a tenant-scoped table whose WHERE clause never mentions tenantId. A bare UPDATE app_assignments SET ... WHERE userId = $1 is a cross-tenant data-isolation bug, not a slow query, and this check catches it whether or not a join is involved. Second, specific to joins: ON conditions on tenant-scoped tables where tenantId is missing entirely, or present only as a correlated reference (c.tenantId = p.tenantId) rather than a bound parameter (c.tenantId = :tenantId). Text analysis can&#39;t tell whether some other part of the query independently anchors that tenant (that requires understanding the query&#39;s logic, not just its syntax), so this check flags the pattern conservatively. Pass 2 is what actually confirms whether it&#39;s fragile in practice.</p><p><strong>Runs locally and in CI on every PR that touches the data access layer. </strong>This isn’t a separate audit step or a tool you run when you remember to. It runs as part of the existing repository test suite: automatically triggered whenever code changes in the data access layer. Locally while a developer is writing code, and in CI on every pull request. The same test that verifies <em>what</em> a query returns also verifies <em>how</em> it runs.</p><p>The key property is that the tool is inherent to how we test the data access layer. When a developer writes a new repository method, they write a component test for it. That’s standard practice. That test automatically runs the query through the logger, captures the raw SQL, and triggers the three analysis passes. There’s nothing to remember, no opt-in, no separate step. Every new query written in the system is covered from the moment its first test is written. Coverage grows with the codebase, automatically, for free.</p><p><strong>One important detail</strong>: the CI database isn’t seeded with millions of rows, but it isn’t tiny either. Pass 1 is the one that actually needs that: a sequential scan only fails the build once the table it touches crosses a row-count threshold, since Postgres genuinely prefers a seq scan on a truly tiny table and flagging that would be a false positive. Below that threshold, a real missing-index bug could pass silently, which is why every repository spec seeds its shared tables with realistic bulk data before any test runs, rather than relying on the few rows each test inserts for its own scenario. Pass 2 and Pass 3 wouldn’t need that regardless: checking SQL text and correlation-only plans are structural properties of the query, present identically at a thousand rows or a million.</p><p>The entire system is programmatic: no AI, no inference, no external services. Pure SQL, EXPLAIN output, and text analysis. It runs completely offline, which means it works on a developer’s laptop the same way it works in CI. The results are consistent: given the same seed data, what you see locally is what CI will see.</p><p>Every finding fails the build unless explicitly acknowledged with a (kind, method, table) triple. When production code is later fixed, the acknowledgment becomes stale; the build fails again until it&#39;s removed. The record stays honest in both directions.</p><h3>Taking It Further</h3><p>The CI approach gets you far. But there’s a gap it can’t close: even with Pass 2 forcing the planner off Nested Loop, the test database has thousands of rows. The planner’s cost estimates (which drive index selection, parallelism decisions, memory allocation) are still based on a tiny table. Some decisions only become visible when the statistics reflect real production data.</p><p>So we took the same repository component tests and started running them once a week against a large-scale synthetic database that mirrors our production data volumes. No simulation, no tricks: just the actual planner operating on actual statistics. A query that passed every CI check at small scale runs against tens of millions of rows and either holds up or doesn’t. Structural warnings from Pass 2 get confirmed or cleared. New failure modes that only emerge at real scale appear before they reach production.</p><p>This gives three tiers of coverage:</p><ul><li><strong>Every PR </strong>— small seed data, structural detection via the no-Nested-Loop fragility check</li><li><strong>Weekly</strong> — production-scale database, real planner decisions at real statistics</li><li><strong>Production monitoring</strong> — slow queries detected at runtime, EXPLAIN run automatically, ticket opened with the query, its EXPLAIN plan, and the violation type</li></ul><p>The on-call engineer gets context, not just an alert: the query, its EXPLAIN output, and a structured fix recommendation. The developer gets a failed build before the code merges. The weekly run catches everything in between.</p><p>The tool that started as a CI gate became a full coverage loop — from the first keystroke to production.</p><h3>The Takeaway</h3><p>The ORM writes <em>correct</em> SQL, not necessarily <em>efficient</em> SQL at scale. It has no opinion on your indexes, no awareness of your row counts, and no idea what the query planner will decide when the table grows. That gap is invisible by design. And invisible gaps don’t show up until something breaks.</p><p>The real problem isn’t any single bad query. It’s that there was no mechanism to know. Queries shipped, environments scaled, the cost became visible — and we found out in production. A one-time audit doesn’t fix that. You need a system that runs on every PR, covers every query path in your codebase, and enforces standards going forward automatically, so coverage grows with the code, not with the incidents.</p><p>The queries were always there. We just needed to start reading them.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=35374ea07fff" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/the-orm-trap-how-we-started-catching-database-problems-before-production-35374ea07fff">The ORM Trap: How We Started Catching Database Problems Before Production</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Detecting Coordinated Fraud: From Shared Attributes to Interactive Graphs]]></title>
            <link>https://medium.com/transmit-security-engineering/detecting-coordinated-fraud-from-shared-attributes-to-interactive-graphs-6e4f1822dc06?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/6e4f1822dc06</guid>
            <category><![CDATA[react-flow]]></category>
            <category><![CDATA[data-analysis]]></category>
            <category><![CDATA[fraud]]></category>
            <category><![CDATA[fraud-detection]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[Bartosz Szafran]]></dc:creator>
            <pubDate>Sun, 17 May 2026 06:53:09 GMT</pubDate>
            <atom:updated>2026-05-19T14:51:26.570Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*70EmApoR7GHyN26D-_6Xyg.png" /></figure><p>By <a href="https://medium.com/u/c084cba47350">Bartosz Szafran</a> &amp; <a href="https://medium.com/u/1333935a1da9">Adi Dimant</a><br><em>Engineering Team, Transmit Security</em></p><p>Fraudsters often reuse resources that legitimate users rarely share — different accounts logging in from the same device or different devices connecting from the same IP address. When multiple overlaps occur, they form networks that may signal coordinated fraud.</p><p>At Transmit Security, we built a system to identify these networks. In this post, we’ll explore how we detect these connections, filter out noise to focus on the most suspicious patterns and present the results as an interactive graph. The article is divided into three parts: problem statement, detection and interface.</p><h3>Problem statement</h3><p>Our systems monitor and collect user events as they interact with protected applications. In each user action, we may have a range of features and attributes — from user account data and device identifiers to behavioral signals like keyboard typing speed. Given a set of these actions, we would like to correlate them to identify coordinated fraud.</p><p>The result is what we call a fraud ring — a network of entities connected through shared attributes, visualized as an interactive graph.</p><h3>Detection</h3><p>The detection of fraud rings is a multi-stage process. Raw action data is transformed into a graph structure, where connected components reveal clusters of related entities. Let’s walk through each stage of the detection.</p><h3>Pipeline overview</h3><p>Fraud ring detection runs as a batch pipeline at regular intervals. In each run, the system processes new actions and merges them with historical data to get a continuous view of fraud activity. The pipeline is written in Python based on Apache Beam, and relies on NetworkX for graph and network analysis. The full graph is processed in-memory in each run, and persisted to disk across runs.</p><p>The pipeline follows a two-stream architecture:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p9x_H3btMCAHdGvsPSju5A.png" /></figure><h4>Input phase</h4><ol><li>Accumulator stream — fraud rings detected in a previous run, loaded from file storage</li><li>Actions stream — actions from the last batch, queried from the database</li></ol><h4>Detection phase</h4><ol><li>Both streams are turned into graphs</li><li>Both graphs are merged (edge weights are summed, per-node action counts accumulate)</li><li>Connected components are extracted from the merged graph. Each connected component represents a potential fraud ring</li><li>Components are filtered to remove noise</li><li>Each surviving connected component is assigned a fraud ring ID</li></ol><h4>Output phase</h4><ol><li>The filtered components are saved as the new accumulator</li><li>Aggregations (per-entity statistics) are published to our ML detection models</li><li>Alerts are sent for significant new fraud rings</li></ol><p>This architecture allows fraud rings to grow over time — each run builds on the accumulated history.</p><h3>Building the graph</h3><p>The first step in detecting a fraud ring is constructing a graph that represents the relationships between entities. This graph becomes the foundation for all analysis.</p><p>The system queries an actions database to get all actions from the latest time interval. Each action includes a set of attributes. These attributes define the nodes in the graph, and their occurrence in multiple actions creates edges between them. The construction process works as follows:</p><ol><li><strong>Node creation</strong> — for each unique entity found in the action attributes.</li><li><strong>Edge creation</strong> — for each action, we create edges between every pair of entities present in that action.</li><li><strong>Edge weighting</strong> — each edge has a weight representing how many actions created that connection. For example, if 2 attributes appeared together in 5 actions, their edge weight is 5. Higher weights indicate stronger relationships between entities.</li></ol><p>This process produces a graph where connected components represent potential fraud rings. A single detection run may identify multiple disconnected clusters, each creating a separate candidate.</p><h4>Example: from actions to graph</h4><p>Let’s walk through an example. Suppose we have four actions:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/574/1*jtb4BOUTi0pKbwG8K0o_eg.png" /></figure><p>Each action’s attribute value becomes an entity in the graph. Then the action itself creates connections between these four entities. From the actions presented in the table, we extract:</p><p><strong>Nodes (9 total):</strong></p><ul><li>3 names: Sarah, John, Emma</li><li>3 home addresses: Oak Lane, Riverside Plaza, Maple Street</li><li>3 types of car: silver sedan, blue hatchback, red SUV</li></ul><p><strong>Edges (up to 12 unique edges, 3 per action — repeated connections increase weight):</strong></p><ul><li>Action 1: Sarah — Oak Lane, Sarah — silver sedan, Oak Lane — silver sedan</li><li>Action 2: John — Riverside Plaza, John — silver sedan, Riverside Plaza — silver sedan</li><li>Action 3: John — Oak Lane, John — blue hatchback, Oak Lane — blue hatchback</li><li>Action 4: Emma — Maple Street, Emma — red SUV, Maple Street — red SUV</li></ul><p>The resulting graph reveals the suspicious pattern:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WL1m0z9TFX-8iRPxLS_eBw.png" /></figure><p>Notice how the silver sedan connects Sarah and John — this is a potential red flag. Additionally, Oak Lane is shared between Sarah (via Action 1) and John (via Action 3). These shared resources are what link them into a fraud ring.</p><p>This way, we’ve built a graph that links entities through shared attributes observed in the same actions — previously unknown relationships now seen as connections in the structure. The next step is to extract the connected components.</p><h3>Extracting connected components</h3><p>To find connected components, we call networkx.connected_components, which runs the BFS algorithm. Two nodes end up in the same component only if there’s a path between them.</p><p>From this point on, the pipeline stops working on one graph and starts working on connected components. Filtering, ID assignment, and storage all run per-component, independently and in parallel.</p><h3>Filtering components</h3><p>Once connected components are identified, they’re filtered to focus on real threats. Not every cluster of connected entities represents fraud — some are noise, some are legitimate shared infrastructure. The filtering process separates signal from noise.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/653/1*Or38l8eqRFHziCqzZjC3dw.png" /></figure><p>The system filters components based on various criteria, such as:</p><h4>Cluster size</h4><p>Very large clusters typically indicate legitimate shared infrastructure (such as a generic public IP), where high connectivity represents noise rather than coordinated fraud. On the other hand, very small clusters often represent legitimate relationships — for example, two users sharing the same Wi-Fi network.</p><p>To address this, the system applies a maximum size threshold (max_size_threshold) unconditionally, and filters clusters below min_size_threshold only when their earliest activity is before cutoff. New small clusters are kept so they can grow on later runs.</p><h4>Node trust rate</h4><p>Each node may be associated with multiple actions in our system. The system evaluates these actions and establishes a trust level for them using various methods, such as heuristics and AI models. These trust levels are then combined to calculate an overall trust level of the node.</p><p>If more than trust_rate_threshold% of entities in a cluster have been trusted, the cluster is filtered out. A high trust rate typically indicates known legitimate users.</p><h4>Activity age</h4><p>Inactive clusters may have been abandoned or already addressed. Components with no new activity for more than retention_threshold days are filtered out.</p><h3>Persisting and restoring the accumulator</h3><p>The pipeline needs to preserve the detected fraud rings between runs. We write them to a file storage as the accumulator — the same artifact that feeds the “Accumulated components from previous run” input in the pipeline diagram.</p><p>We had two options for the on-disk data structure:</p><ul><li>one big graph object containing every component,</li><li>each connected component is saved as its own self-contained graph.</li></ul><p>Two fraud rings never share an edge, because any connection between them would merge them into one. That means both forms would contain exactly the same information.</p><p>We chose the second option because the pipeline already handles components individually. Every per-component step — filtering, ID assignment, writing — runs independently, since components share no state. Storing one component per record matches this structure directly, without an intermediate step to merge everything into one big graph object again.</p><p>Note that we store only components that passed the filter — everything else is dropped and won’t appear in the next run.</p><p>The next run picks up from the accumulator. Before processing new actions, the pipeline reads the accumulator records in parallel and decodes each line back into a single-component graph. Together they form an in-memory universe of fraud rings carried over from previous runs. Because components never share an edge, building the universe is straightforward: each loaded component is added to a shared graph object, sitting alongside the others without overlap.</p><p>The universe is then merged with the graph built from new actions. From there the cycle repeats: connected components are re-extracted from the merged graph, filtered, and stored again as the next accumulator.</p><h3>Tracking fraud rings across runs</h3><p>Fraud rings evolve between runs. From the system’s perspective, every component coming out of a run falls into one of three cases:</p><ol><li><strong>A brand new ring</strong> — it didn’t exist in the previous accumulator.</li><li><strong>An existing ring that grew</strong> — new actions added entities or strengthened existing connections, but the ring is still the same ring.</li><li><strong>A merger of two or more existing rings</strong> — a new shared entity has connected previously separate rings, collapsing them into one.</li></ol><p>To make this evolution useful (tracking growth, updating the UI, avoiding duplicate notifications) we need to recognize the same ring across runs. We assign every fraud ring a unique ID and attach it to every node in the ring. The ID is part of the components data we write to the accumulator, so it’s recovered when the next run loads the universe. This gives each node two pieces of identity: the entity itself (a name, a home address, a type of car) and the ID of the ring it currently belongs to.</p><p>The three cases translate directly into how IDs are assigned. After extracting and filtering each component, the system looks at the IDs already attached to its nodes:</p><ol><li><strong>Brand new ring</strong> — none of the nodes carry an ID. We generate a fresh UUID and attach it to every node.</li><li><strong>Existing ring grows</strong> — every node in the component has either the same ID or no ID. We keep that ID and attach it to any nodes without one.</li><li><strong>Rings merge</strong> — nodes carry two or more different IDs. We pick the ID held by the most nodes (effectively the largest of the pre-merge rings) and overwrite every node in the component with it. The other IDs are dropped.</li></ol><h3>User interface</h3><p>Fraud rings can grow large, containing thousands of nodes. Visualizing them is essential to investigate and act on threats. The most significant fraud rings are promoted to the Mosaic Portal, where users are notified and can investigate.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ASzykSJbNEfx5j0mO1tX0w.gif" /></figure><h3>Rendering at scale: two decisions</h3><p>Displaying a graph of thousands of nodes in the browser required two separate decisions:</p><ol><li><strong>Layout</strong> — positions for each node must come from somewhere. We can compute them ahead of time on the backend, in a web worker, off-screen on the main thread, or live as the simulation runs.</li><li><strong>Rendering</strong> — how to draw. The graph can be rendered as HTML/SVG DOM elements, on a 2D canvas, or through WebGL.</li></ol><h3>Layout: off-screen force simulation</h3><p>We use D3’s <a href="https://d3js.org/d3-force/simulation">force simulation</a> to automatically arrange nodes. The algorithm works in an iterative way, revealing the network structure. It’s a physics-based approach where nodes push each other apart while edges pull connected nodes together.</p><p>We considered a few ways to run this computation:</p><ul><li><strong>Live-animated layout</strong> — re-render the graph on every simulation tick. The animation is visible to the user, but for a graph with hundreds of nodes and hundreds of iterations, every tick triggers a full re-render. The browser can’t keep up and the UI starts to lag.</li><li><strong>Off-screen, main thread</strong> — run the simulation in memory. We draw the result only when the calculation ends, so there’s exactly one commit regardless of iteration count. The user sees no graph until the simulation finishes, but because the total number of iterations is known, progress is deterministic and we can display a progress bar until the final layout is ready.</li><li><strong>Web Worker</strong> — move the simulation off the main thread. It could be valuable for massive graphs, but at a scale of a few thousand nodes, the off-screen main-thread calculation takes less than a few seconds. The simulation schedules each iteration separately, so the main thread stays responsive between them. The extra complexity isn’t justified.</li><li><strong>Server-side precomputation</strong> — store layout coordinates with each ring. We rejected this because layout parameters are UI-specific concerns and may vary, for instance, depending on the viewport size.</li></ul><p>We went with the off-screen, main-thread approach — a good trade-off between implementation effort and rendering performance.</p><p>Our simulation combines three forces:</p><ul><li><strong>Center force</strong> — pulls all nodes toward the center, preventing the network from growing too wide.</li><li><strong>Charge force</strong> — makes nodes push each other apart, creating space between them.</li><li><strong>Link force</strong> — pulls the nodes closer based on their edges.</li></ul><p>As a result, densely connected entities show up together and loosely connected groups spread apart. This makes it easier to spot suspicious patterns visually.</p><h3>Rendering: HTML/SVG over Canvas and WebGL</h3><p>Our graph has specific interactivity requirements: each node has a tooltip with details, a highlighted border when selected, and click handlers that open the details side panel. Fraud rings contain at most a few thousand nodes.</p><p>We considered three rendering approaches:</p><ul><li><strong>HTML/SVG</strong> — each node and edge is a DOM element. The interactions are straightforward to implement, since the browser maps clicks and hovers to the right node automatically. The trade-off is performance — DOM trees start to lag at tens of thousands of nodes.</li><li><strong>2D Canvas</strong> — draw the entire graph into a single canvas element. Rendering scales to tens of thousands of nodes, but the canvas is one opaque pixel buffer to the browser — detecting which node the pointer is on, hover effects, and tooltips have to be implemented manually.</li><li><strong>WebGL</strong> — offload rendering to the GPU. Performance is the highest, easily handling hundreds of thousands of nodes, but it’s the most complex solution. As with canvas, all interactivity has to be implemented manually.</li></ul><p>At our scale, HTML/SVG performance is sufficient and interactivity wins the trade-off. We chose <a href="https://reactflow.dev/">React Flow</a> — an HTML/SVG-based graph library — rather than building one ourselves.</p><p>Off-screen layout has already removed the per-tick render cost, leaving just a single commit to render.</p><h3>Floating edges: skipping the trigonometry</h3><p>The default way of connecting edges to nodes in React Flow is through anchors at specific points of the node — right, top, bottom, or left. This works well for rectangular nodes. Fraud ring nodes, however, are circular and can receive connections from any direction.</p><p>React Flow’s official <a href="https://reactflow.dev/examples/edges/floating-edges">floating edges</a> example solves this with trigonometry: for each edge, compute where the line intersects each node’s circular perimeter. We use a simpler approach — draw each edge directly from the center of one node to the center of the other. The opaque circular nodes cover the line endpoints, so the result looks the same as a proper perimeter intersection. The shortcut is simpler to implement and faster to render.</p><h3>Summary</h3><p>The fraud ring detection system builds graphs from attributes collected by our systems, linking accounts that share the same resources. By filtering out noise and evolving over time, it becomes an effective tool for identifying and visualizing suspicious patterns — making coordinated fraud easier to detect.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6e4f1822dc06" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/detecting-coordinated-fraud-from-shared-attributes-to-interactive-graphs-6e4f1822dc06">Detecting Coordinated Fraud: From Shared Attributes to Interactive Graphs</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Inside Auto Capture: How We Engineered Real-Time Document and Selfie Capture Across Every Platform]]></title>
            <link>https://medium.com/transmit-security-engineering/inside-auto-capture-how-we-engineered-real-time-document-and-selfie-capture-across-every-platform-ce3a335029bb?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/ce3a335029bb</guid>
            <category><![CDATA[vision]]></category>
            <category><![CDATA[autocapture]]></category>
            <dc:creator><![CDATA[Gal Yona]]></dc:creator>
            <pubDate>Mon, 06 Apr 2026 08:53:06 GMT</pubDate>
            <atom:updated>2026-04-06T08:53:05.304Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Gal Yona &amp; </strong><a href="https://medium.com/u/97fdbef77ef4"><strong>Ophir Yaniv</strong></a></p><p><em>Engineering Team, Transmit Security</em></p><p>This is the second post in our engineering blog series. In this post, we’ll explore the engineering behind real-time document and selfie capture, from on-device ML models to cross-platform challenges and the architecture that ties it all together.</p><p>You’ve probably done it a dozen times: pointed your phone camera at an ID card, tapped the shutter button, and… blurry. Glare. Half the card cut off. Try again.</p><p>Auto Capture exists to kill that frustration. It guides users in real time and automatically captures clear images of documents and selfies, no manual shutter tap required. But making that feel effortless took an on-device ML pipeline, platform-specific inference runtimes, backend-driven model selection, and enough edge cases to keep an engineering team caffeinated for months. Let’s break it all down.</p><h3>Two Flows, One Goal: Understanding What Auto Capture Actually Does</h3><p>Before we dive into the pipeline, it’s worth understanding that Auto Capture isn’t a single monolithic system. It’s two distinct flows that share a common goal: capturing the best possible image without burdening the user.</p><p>Both flows answer the same fundamental questions for each camera frame: Is the relevant object present, a document or a face? Is it properly positioned? And is the image suitable for downstream identity verification, whether that means checking for blur, glare, face rotation, or profile angle?</p><p><strong>Document Auto Capture</strong> gates capture readiness for the front and back of ID cards and driver’s licenses, or just the front for documents like passports. At its core, on-device ML models handle two jobs: a document detector locates and validates the document in the frame, while a dedicated classifier evaluates image quality for blur and glare in real time. Only when both checks pass is capture allowed. Once captured, the image is sent to the backend where the real orchestration begins. For front-side captures, the backend runs document classification, identifying the document type and determining whether a back-side capture is needed (and whether that back is expected to carry a barcode). If it is, the SDK transitions the user into a back-side capture flow, running through the same on-device quality gate before sending the image for backend processing, including classification matching and barcode decoding. To the user, it’s one smooth flow. Behind the scenes, the backend is driving a multi-step acquisition pipeline, but it’s the on-device capture gate, running on every frame for both sides, that makes the experience feel instant and intelligent.</p><p><strong>Selfie Auto Capture</strong> is a separate flow built around face detection with quality and composition checks. Rather than image quality classification, it gates capture on face presence, pose, positioning, and occlusion. It carries its own set of real-world challenges: people blink, move, wear sunglasses, and generally make the camera’s job harder than a flat card ever would.</p><p>Both flows evaluate frames on-device, giving users real-time feedback without a round trip to the server, keeping latency low and the experience smooth.</p><h3>Model Compression: Fitting ML on a Mobile Device</h3><p>Before we get into the specifics of each pipeline, let’s talk about the elephant in the room, or rather, the elephant you’re trying to fit into a phone. Running ML models on-device means working within strict constraints: limited memory, limited compute, and the expectation of real-time performance. Server-grade models that work beautifully in the cloud would melt a phone’s battery and patience.</p><p>Our approach starts at the model architecture level. Rather than simply shrinking server-grade models after the fact, we develop and select architectures that are fundamentally designed for mobile inference, lightweight by nature, not just by compression. This means choosing model families and structures that deliver strong accuracy within the compute and memory budgets of mobile devices from the ground up.</p><p>On top of that, we apply quantization, reducing model precision from 32-bit floating point to lower-precision representations, which further shrinks model size and accelerates inference with minimal accuracy loss. The trade-off is subtle: quantization artifacts can appear in edge cases, so each quantized model goes through rigorous validation against the full-precision baseline.</p><p>Each target platform also has its own export and conversion pipeline (Core ML, TFLite, TensorFlow.js), and each conversion step is an opportunity for runtime-specific optimization. Operator fusion, graph simplification, and format-specific tuning all contribute to squeezing out additional performance during the export process.</p><p>As we develop new model versions, each goes through this export and validation pipeline per platform, ensuring the optimized models meet quality thresholds on every target runtime before they reach a single device.</p><h3>Document Auto Capture: The On-Device Quality Gate</h3><p>The document Auto Capture pipeline is, at its heart, a quality gate. The device continuously streams camera frames, and for each frame, the system asks one critical question: is this image good enough to capture?</p><h3>Finding the Document First: The Document Detector</h3><p>Before we can judge image quality, we need to know there’s actually a document to judge. The pipeline starts with a document detector that locates the document within each camera frame, identifying its boundaries and confirming it’s sufficiently visible and properly framed.</p><p>This might sound trivial (it’s a card, how hard can it be?), but users have a remarkable talent for creative camera angles, partial framing, and holding documents at distances that would challenge even the most advanced ML model. The detector provides the foundation that everything else builds on: no detected document, no point evaluating blur or glare. It also feeds real-time guidance back to the user (“move the document into frame,” “hold it closer”) so they know what to fix before the quality checks even kick in.</p><h3>The Blur/Glare Classifier: Gatekeeper of Image Quality</h3><p>Once the detector confirms a document is present and properly framed, the primary capture decision runs through a MobileNetV2-based classifier purpose-built for the Auto Capture use case. For every candidate frame, this model evaluates the image and outputs one of several classes: success (good quality), blur, glare, or bad-quality.</p><p>If the image passes, capture is triggered. If it doesn’t, the frame is rejected, the user receives real-time guidance (“hold steady,” “move to better lighting,” etc.), and the device keeps scanning. This loop runs continuously until a quality frame is captured, which to the user feels like the camera just “knows” when the shot is right.</p><p>Why is this a dedicated ML model rather than a simple heuristic? Because blur and glare are surprisingly context-dependent. A Laplacian variance check might work in a lab, but real-world conditions, variable lighting, moving hands, glossy vs. matte card surfaces, demand a learned classifier that generalizes across environments. The model was trained on a wide range of real-world capture conditions, which gives it robustness that heuristics with hand-tuned thresholds simply can’t match.</p><p>The classifier’s thresholds are configurable, which becomes important when we talk about backend-driven configuration later. What counts as “too blurry” isn’t a universal constant. It depends on the downstream processing requirements, the document type, and the capture environment.</p><h3>After Capture: Backend Acquisition</h3><p>Once the on-device quality gate approves a frame, the image is sent to the backend for acquisition, heavier processing that doesn’t need to run at camera frame rates. The backend handles document classification, determines whether a back-side capture is needed, and runs barcode decoding to extract structured data from formats like PDF417 on driver’s license backs. It can also perform alignment and refinement, including segmentation-based background removal and perspective correction.</p><p>This split between on-device capture gating and backend acquisition is a key architectural decision: keep the device focused on one fast operation (quality classification), and let the server handle the rest.</p><h3>Selfie Auto Capture: Composition Over Classification</h3><p>Selfie Auto Capture tackles the same core questions (object present, properly positioned, suitable for verification) but through a different lens (pun mildly intended). Where document capture centers on image quality classification, selfie capture focuses on face quality and composition: Is there a face? Is it properly framed? Will it hold up for identity verification?</p><h3>The Face Detection Foundation</h3><p>Every frame starts with face detection. No face found? Rejected. Multiple meaningful faces detected (when that check is enabled)? Also rejected: the system needs exactly one clearly visible face. When a valid detection comes through, the pipeline evaluates the largest face candidate and moves on to the quality checks.</p><h3>Fast Geometric and Pose Checks</h3><p>From the detector output, bounding box and landmarks, the pipeline runs a series of lightweight checks that provide immediate capture guidance:</p><p><strong>Geometry and positioning checks</strong> ensure the face isn’t too close, too small, too close to the image borders, or cropped. These are derived directly from the bounding box and landmark geometry that the face detector already provides, making them computationally cheap to evaluate.</p><p><strong>Pose checks</strong> reject frames where the face is rotated or angled beyond acceptable thresholds. Identity verification needs a reasonably front-facing capture, and catching bad angles at capture time saves users from a rejection further downstream. Nobody wants to learn their selfie was unusable after they’ve already waited for a server round-trip.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ys6MHKH-wnBP2dQK-754gw.png" /></figure><p>These checks are deliberately fast and heuristic-based. Face detection gives us rich enough geometry to derive these quality signals without additional inference passes, which is exactly what you want when you’re evaluating frames at camera speed on a mobile device.</p><h3>Server-Side Model-Backed Quality Checks</h3><p>Some conditions can’t be caught by bounding box math alone. Once a frame passes the on-device geometric checks and reaches the backend, dedicated neural network models handle the trickier quality conditions:</p><p><strong>Closed eyes detection</strong> uses a model to determine whether the user’s eyes are shut. Squinting, heavy eyelids, and the wide variation of eye shapes across populations make this a problem that genuinely benefits from a learned classifier rather than a geometric heuristic.</p><p><strong>Face occlusion detection</strong> uses a classifier to catch obstructions: sunglasses, hands, masks, or a particularly enthusiastic scarf. Rather than trying to enumerate every possible occlusion through rules (a losing game given the creativity of real-world users), the model classifies whether the face is sufficiently unobstructed for identity verification.</p><p>Running these checks server-side is a deliberate architectural choice. The models benefit from more compute headroom than a mobile device can comfortably offer, and keeping them on the backend means we can update models and tune thresholds without pushing a new app release.</p><p>The combination of fast on-device geometric filtering and server-side model-backed checks creates an autocapture gate that catches problems at two stages, giving users instant feedback on positioning while quietly ensuring the deeper quality conditions are met before a selfie moves on to identity verification.</p><h3>Cross-Platform Challenges: One Pipeline, Three Worlds</h3><p>Here’s where things get spicy. Auto Capture needs to run on iOS, Android, and the Web. Same user experience, same quality bar, but completely different runtime environments under the hood.</p><h3>iOS: Core ML</h3><p>Apple’s Core ML framework provides tight integration with the Neural Engine and GPU on iOS devices. Models are converted to Core ML format with quantization support, enabling hardware-accelerated inference optimized for Apple’s silicon. The upside: excellent performance on modern iPhones and iPads. The challenge: Core ML has its own set of operator support limitations and quantization behaviors, so model conversion isn’t always a straightforward export.</p><h3>Android: TensorFlow Lite</h3><p>On Android, models are exported to TensorFlow Lite, which can leverage device-specific acceleration paths depending on the hardware and integration. TFLite’s flexibility is essential given the enormous range of Android devices, from flagships to budget phones that are, charitably, doing their best. The diversity of the Android ecosystem means more testing, more edge cases, and more creative optimization to ensure consistent behavior across device classes.</p><h3>Web: TensorFlow.js with WebAssembly Backend</h3><p>On the Web, models are delivered via TensorFlow.js, with WebAssembly backend assets available for CPU-oriented execution in-browser. While WebGL and WebGPU can provide some acceleration in certain browser contexts, the WASM path offers the most consistent cross-browser behavior.</p><p>This means the Web models need to be the leanest variants, and inference must be carefully optimized to maintain real-time frame rates without turning the user’s browser tab into a space heater.</p><h3>The Conversion Tax</h3><p>Each platform’s runtime expects models in its own format, which means maintaining conversion pipelines from the base trained model to Core ML, TFLite, and WASM-compatible formats. Each conversion introduces its own quirks: operator compatibility, numerical precision differences, and performance characteristics that need to be validated per platform. It’s not glamorous work, but it’s the kind of infrastructure that makes or breaks a cross-platform ML product.</p><h3>Dynamic Model Loading: The Right Model for the Right Device</h3><p>Not all devices are created equal, and pretending otherwise leads to either poor performance on low-end devices or wasted capability on high-end ones. We solve this with a backend-driven dynamic model loading and caching pipeline.</p><p>Here’s how it works: the backend maintains a device profile mapping that associates device characteristics with the most suitable model variant. When the SDK initializes, it reports its device profile, and the backend responds with the appropriate model configuration, selecting the variant that balances accuracy and performance for that specific device class.</p><p>Models are downloaded and cached locally, so subsequent sessions don’t pay the download cost again. If a new model variant is available (say, we’ve shipped an improved blur/glare classifier), the backend can push the update without requiring a new app release.</p><p>This architecture gives us several powerful capabilities:</p><p><strong>Silent updates.</strong> Model improvements, threshold adjustments, and even new model architectures can be deployed without going through app store review cycles.</p><p><strong>A/B testing.</strong> We can roll out model variants to subsets of devices and measure real-world performance before a full rollout.</p><h3>Backend-Driven Configuration: Tuning Without Shipping</h3><p>Beyond model selection, the backend also controls runtime configuration for the capture pipeline. This includes thresholds for blur and glare sensitivity, retry logic parameters, capture timeouts, and other behavioral settings.</p><p>Why does this matter? Because the “right” threshold isn’t universal. Each customer operates in a different environment: some deployments are predominantly indoor (think bank branches or office onboarding), while others are outdoor or mixed (field agents, remote identity verification on a park bench). A blur threshold tuned for controlled indoor lighting might be too aggressive for outdoor captures with natural lighting variation. A glare threshold that works well for matte ID cards might reject too many frames when users have glossy laminated documents.</p><p>This is configuration as a feature, not a workaround. Different customers, document types, and capture environments have different sweet spots, and those sweet spots can shift over time. Backend-driven configuration lets us continuously optimize the capture experience based on real-world performance data, adapting thresholds and retry logic to each deployment’s specific needs without requiring an app update.</p><h3>Observability: You Can’t Improve What You Can’t Measure</h3><p>Auto Capture emits detailed metrics at each stage of the pipeline. These events give us visibility into model performance and user behavior in production, not just in controlled test environments.</p><p>We track signals across the capture lifecycle: quality gate pass/fail rates, blur and glare rejection frequency, selfie composition rejection reasons, overall capture success rates, and user interaction patterns like how many attempts it takes to get a successful capture.</p><p>This data feeds a continuous improvement loop. It tells us where users are struggling, which device classes underperform, whether a model update improved real-world outcomes, and where our thresholds need adjustment. Without this observability, we’d be flying blind, shipping models into production and hoping for the best.</p><h3>What Makes It All Click</h3><p>Auto Capture might look simple from the outside: point your camera, get a good image. But underneath that simplicity is a carefully designed system where an on-device quality gate, platform-native inference runtimes, backend-driven intelligence, and server-side acquisition processing each play their part.</p><p>The architectural split between on-device and backend is perhaps the most important decision in the whole system. By keeping the device-side pipeline focused and fast (a quality classifier for documents, a composition checker for selfies), we deliver the real-time responsiveness users expect. By handling the heavier lifting, classification, alignment, barcode decoding, on the backend, we get access to more compute, easier updates, and a cleaner separation of concerns.</p><p>The other engineering decisions, using platform-native inference runtimes, delegating model selection to the backend, keeping configuration server-driven, and building deep observability, aren’t accidental either. Each one reflects a real lesson learned from operating ML at scale across a fragmented device ecosystem.</p><p>We’ll continue exploring more of our engineering challenges in future posts. Stay tuned.</p><p><em>Gal Yona and Ophir Yaniv are engineers at Transmit Security, where they work on the identity verification platform that processes millions of document and selfie captures across the globe.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ce3a335029bb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/inside-auto-capture-how-we-engineered-real-time-document-and-selfie-capture-across-every-platform-ce3a335029bb">Inside Auto Capture: How We Engineered Real-Time Document and Selfie Capture Across Every Platform</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Architecting for Scale: Session Separation in Modern Authentication Platforms]]></title>
            <link>https://medium.com/transmit-security-engineering/architecting-for-scale-session-separation-in-modern-authentication-platforms-5c518dac2df5?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/5c518dac2df5</guid>
            <category><![CDATA[refactoring]]></category>
            <category><![CDATA[scalability]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[resilience]]></category>
            <category><![CDATA[performance-testing]]></category>
            <dc:creator><![CDATA[Ido Kislev]]></dc:creator>
            <pubDate>Wed, 18 Mar 2026 15:11:22 GMT</pubDate>
            <atom:updated>2026-03-18T15:41:31.431Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XpY7k1EuHJ4_5BsnxcX2AQ.png" /></figure><h3>Architecting for Scale</h3><p><em>Decoupling Session Management in FlexID: Architecture Evolution and Operational Insights</em></p><p>Written By: <a href="https://medium.com/u/b7952d34eba1">Alex Brus</a> and <a href="https://medium.com/u/11d98e38f7c6">Ido Kislev</a></p><h3>Introduction</h3><p>In authentication systems, session management is critical infrastructure: sessions track who is authenticated, from which device, and maintain security context throughout a user’s interaction. Few years ago, as we were helping a customer to deal with a particularly high-scale deployment, it became apparent that the FlexID session storage architecture had a fundamental coupling between user- and session-data that limited the system’s ability to scale.</p><p>And so, what started as a performance investigation became a comprehensive refactoring effort that spanned a significant part of the system’s codebase, involving foundational changes to the codebase to introduce a context-based architecture that enables decoupling of session and user storage. This in turns allows independent scaling, flexible storage backends, and zero-downtime migration.</p><p>This post details the journey of architecturally separating session storage from user storage in FlexID. Developers and architects will find technical insights into executing large-scale architectural refactoring in a mission-critical system, while maintaining backward compatibility and zero downtime.</p><h3>The Problem: When Tight Coupling Becomes a Performance Tax</h3><p>FlexID was built around a document store as its primary OLTP data store. When modeling the data store we originally followed paradigm that’s commonly applied for document stores: store data that is used together in the same document. Hence, sessions were embedded directly within user documents. From a data modeling perspective this made sense: sessions belong to users, so why not store them together?</p><figure><img alt="The legacy data model: sessions nested within devices and devices nested within users" src="https://cdn-images-1.medium.com/max/1024/1*dUW09EZ9hOFeCwpluf55Mw.png" /><figcaption><strong>The legacy data model: sessions nested within devices and devices nested within users</strong></figcaption></figure><pre>{<br>  &quot;User&quot;: {<br>    &quot;id&quot;: &quot;string&quot;,<br>    &quot;email&quot;: &quot;string&quot;,<br>    &quot;applications&quot;: [<br>      {<br>        &quot;Application&quot;: {}<br>      }<br>    ],<br>    &quot;devices&quot;: [<br>      {<br>        &quot;id&quot;: &quot;uuid&quot;,<br>        &quot;hardwareId&quot;: &quot;string&quot;,<br>        &quot;signingKey&quot;: {<br>          &quot;RotationKey&quot;: {}<br>        },<br>        &quot;applicationId&quot;: {<br>          &quot;ApplicationId&quot;: {}<br>        },<br>        &quot;sessionOpt&quot;: {<br>          &quot;Session&quot;: {}<br>        }<br>      }<br>    ]<br>  }<br>}</pre><p>In fact the structure was a little more sophisticated: rather than containing sessions directly within user documents, users contained devices and devices contained sessions. Every authentication request meant loading the full user document, extracting the relevant device, and then reading the embedded session.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/774/1*uIi2rjpSKCyoOUAUIdOzMg.png" /></figure><h3>The Pain Points</h3><p>The first challenge was the <strong>monolithic data model</strong>. Session data lived embedded within user documents, which meant that every session operation, whether a simple validation or a minor update, required loading and writing the entire user record. This led directly to inefficiency: sessions could not be queried independently of users, and their lifecycles became unnecessarily coupled. A session expiring should not require updates to user data, but the FlexID architecture made this unavoidable. Similarly, session validation required loading the entire user document just to check if a token was valid. Simple session updates, such as updating a “last accessed” timestamp, triggered writes to the full user record. Operations that only needed session data implied the cost of loading complete user profiles with their full device history.</p><p>FlexID utilizes a stateless architecture to facilitate significant horizontal scaling. The stateless architecture compounded the problem: every request scoped to a user session (this encompasses most requests to an authentication server, obviously) required loading the session data from the database, and typically also writing it back. Again, as sessions were embedded within user documents, this meant loading/storing the entire user object including all devices and their history on every request.</p><p>It’s important to note that in the original FlexID data modelling, session data comprises a significant portion of the user document. As sessions were created, updated, and expired, the user document would expand and contract significantly causing unpredictable document size fluctuations that complicated database operations and caching strategies. Moreover, completed or expired sessions would be cleaned up asynchoronously to their expiration, leading to a <strong>“dead” session accumulation</strong> problem. This issue was particularly acute for on-premise deployments, due to Transmit’s lack of direct control over the customer configuration or background processes. User cleanup settings were optional, and many customers either didn’t enable them, or only configured them partially. The result? Pending sessions from failed or abandoned authentications remained in user documents indefinitely. Expired sessions stayed in records for months or even years. Some user documents accumulated dozens or even hundreds of dead sessions, causing document sizes to grow unbounded and degrading all database operations reads, writes, sharding, and replication. While this problem is largely obsolete in FlexID SaaS deployments with automatic cleanup processes, on-premise customers with high-scale deployments were potentially significantly impacted.</p><p>Finally, with the original architecture session storage could not be based on a different data store than user storage. Session data and user data are fundamentally different. Session data is highly volatile whereas much of the user data is immutable and the rest is only seldom updated. With the architecture forcing a unified data store for both, the session’s data store could not be independently scaled, nor could it be optimized for ephemeral data. Multi-region deployments were also constrained by unified storage requirements.</p><blockquote><strong>The Breaking Point</strong></blockquote><blockquote>Under load testing simulating some of our largest enterprise customers’ traffic patterns, the operational database reached <strong>60% CPU utilization</strong> on the hottest shard. Session operations consumed resources meant for user operations, and there was limited headroom for traffic spikes.</blockquote><h3>The Solution: Architectural Separation</h3><p>Moving forward required a fundamental architectural change rather than incremental fixes. It was crucial to treat sessions and users as entirely independent domain objects with no embedded references, no shared storage, and no coupled lifecycles.</p><p>The solution was based on four principles:</p><ol><li><strong>Independent domain models</strong>: Sessions and users would become entirely separate objects with no direct references between the objects. Each object could be optimized for their own access patterns without compromising the other.</li><li><strong>Context-based orchestration</strong>: Rather than embedding relationships in the data model, these would be managed through context objects at runtime — load a user from one store, load a session from another, and combine them in a context object for business logic. The storage layer stays clean and the orchestration layer handles complexity.</li><li><strong>Dual-mode operation</strong>: Both legacy and new modes would be supported simultaneously. Configuration-driven switching would enable zero-downtime migration. Simply flip a flag to change storage strategy without code deployment.</li><li><strong>Database flexibility</strong>: Session storage would be based on a pluggable interface using MongoDB initially, then Redis, and also supporting PostgreSQL for deployments with specific datastore requirements.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TWbgKI4duYWMb82Ifyc6sQ.png" /></figure><h3>The Session Repository Interface</h3><p>Sessions are written under a globally unique sessionId. In the repository API we always pass userId alongside it to scope access and validate ownership. For device-bound flows we also support lookup by (userId, deviceId) to retrieve the current active session for that device and enforce the one-session-per-device constraint.</p><p>A clean repository abstraction supporting multiple access patterns was defined. The interface needed to handle lookups by session ID, user ID, and device ID, with each optimized for its specific use case.</p><pre>trait SessionRepository {<br>  // Primary operations<br>  def findById(userId: UserId, sessionId: UserSessionId): Future[Option[UserSession]]<br>  def findByUserDeviceId(userId: UserId, deviceId: UUID): Future[Option[UserSession]]<br>  def findByUserId(userId: UserId): Future[Seq[UserSession]]<br>  <br>  // Lifecycle operations<br>  def save(userSession: UserSession): Future[SaveResult]<br>  def upsertByUserDeviceId(userSession: UserSession, deviceId: UUID): Future[SaveResult]<br>  def delete(userId: UserId, sessionId: UserSessionId): Future[SaveResult]<br>}</pre><h3>How we do (userId, deviceId) lookup in Redis (concrete, matches code)</h3><p>In the Redis implementation we keep <strong>two keys</strong>:</p><ul><li><strong>Session record</strong>: sessions:&lt;sessionId&gt; → JSON payload (with TTL)</li><li><strong>Device index</strong>: user_device_index:&lt;userId&gt;:&lt;deviceId&gt; → &lt;sessionId&gt; (with same TTL)</li></ul><p>On write, we update the session record and the index <strong>in one atomic batch</strong>, and if the device index already points to a different sessionId we delete the old session (replace behavior to enforce the constraint).</p><h3>The New Session Model</h3><p>The session model captures all necessary metadata for independent operation. Key design decisions included optional device binding (supporting both device-bound and web/API sessions), embedded TTL for database-native expiration, realm awareness for multi-tenancy isolation, and session ID as the primary key for direct access without compound keys.</p><pre>case class UserSession(<br>  session: Session,              // The actual session data<br>  realm: Realm,                  // Multi-tenancy context<br>  userId: UserId,                // User association (reference only)<br>  deviceId: Option[UUID] = None, // Optional device binding<br>  ttlDatum: DateTime             // Expiration timestamp for DB-native TTL<br>)</pre><h3>Consistency Considerations</h3><p>Separating storage introduces a critical question: what happens when session data and user data diverge?</p><p><strong>Our Consistency Model:</strong></p><p>Separating session storage introduces a natural question: what happens when session data and user data temporarily diverge? In the embedded model, the user document and the session were updated together as a single record. Once sessions live in a dedicated store, the two objects can momentarily fall out of sync (e.g., a user/device change is committed while a session update is still in flight, or a session lingers briefly after a user is deleted). We address this with an explicit, pragmatic consistency model that matches the reality of authentication traffic: sessions are ephemeral, and correctness is maintained through scoping, TTL, and deterministic convergence rules.</p><p><strong>Our consistency model</strong></p><ul><li><strong>Sessions are eventually consistent with user state</strong>: session reads are always scoped and validated (by userId, and when relevant applicationId/deviceId). If the session doesn’t match the expected scope, we treat it as missing and the client re-authenticates.</li><li><strong>Session operations don’t require user locks</strong>: routine session reads/writes touch only the session record, avoiding contention and lost updates on the user document.</li><li><strong>User deletions trigger async session cleanup</strong>: deleting a user schedules/executes deletion of that user’s sessions. A small lag is acceptable (e.g., a few seconds) because sessions are validated and time-bounded.</li><li><strong>Device binding uses optimistic concurrency</strong>: for device-bound sessions we enforce “one active session per (user, device)” at the session store level (unique constraint/index where deviceId exists) and rely on upsert semantics so concurrent attempts converge to a single outcome.</li></ul><p><strong>Edge case: concurrent login from the same device</strong></p><p>If a user starts two authentications from the same device at the same time, both requests may attempt to create a device-bound session. The session store enforces the single-session-per-device constraint, so one attempt wins and the other retries using an upsert, resulting in a single active session for that (userId, deviceId) pair. This “last writer wins” convergence is acceptable here because these are journey sessions: by the time the flow completes, the system has converged to one canonical session and the client proceeds with that state.</p><h3>Preventing Accidental Session Writes</h3><p>In this new mode, sessions should never be written to device objects. Runtime guards were added that throw exceptions if code attempts to write sessions to the wrong storage. These checks caught several edge cases during development that would have caused subtle bugs in production.</p><h3>Session Constraint Enforcement</h3><p>In the legacy architecture, the one-session-per-device constraint was enforced by document structure and you simply couldn’t have two sessions in the same device object. With separated storage, this needed to be enforced programmatically:</p><ul><li><strong>MongoDB:</strong> Unique compound index with partial filter.</li><li><strong>PostgreSQL:</strong> Unique constraint on (user_id, device_id) where device_id is not null.</li><li><strong>Redis:</strong> In Redis, we maintain a primary key per session (sessions:&lt;sessionId&gt;) and a secondary index mapping each (userId, deviceId) to the current session ID. Writes update the session record and the index together in a single atomic batch, so we never observe a partial state where the index points to a non-existent session (or vice versa).</li></ul><h3>Choosing a Storage Backend</h3><p>Different deployment scenarios call for different storage backends. Three storage providers were implemented for session data, each optimized for a specific use case. The pluggable architecture means organizations can select and switch between backends based on their specific requirements in on-prem deployments, while for a SaaS deployment we apply the suitable solution.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/767/1*SBvD16eK5xf09mc4UwsWXw.png" /><figcaption>All three implementations provide the same interface through the SessionRepository abstraction, enabling transparent switching or migration between backends as requirements evolve.</figcaption></figure><h3>Dual-Mode Architecture: Zero-Downtime Migration</h3><p>Our new architecture supports two operational modes, giving you the flexibility to choose the approach that best fits your deployment requirements. Legacy mode keeps sessions embedded in user documents, while the default mode stores sessions in dedicated storage. This dual-mode design enables zero-downtime migration and instant rollback capability.</p><p>Business logic operates on context objects, completely unaware of which storage mode is active; hence the switch is transparent to all calling code.</p><h3>Migration Strategy: Four-Phase Rollout</h3><p><strong><em>Phase 1</em></strong> <strong>Dark Launch</strong><br> Deploy code with legacy mode. New infrastructure deployed but dormant. Validate dual-mode code paths work.</p><p># All environments initially transient-database: sessions: enabled: false # Legacy mode active</p><p><strong><em>Phase 2</em></strong><em> </em><strong>Internal Validation</strong><br>Enable in dev/staging environments. Run full integration tests in both modes. Validate all session operations. Fix edge cases discovered.<br><strong>Validation</strong>:</p><ul><li>All integration tests pass in new mode</li><li>Performance benchmarks confirm improvements</li><li>Session operations function identically</li></ul><p><strong>Phase 3</strong> <strong>Gradual Production Rollout</strong> Enable for subset of production traffic. Monitor metrics at each stage. Gradual increase: 25% → 50% → 75% → 100%.</p><p># Environment-specific configuration production-cluster-1: transient-database.sessions.enabled: true # 25% traffic production-cluster-2: transient-database.sessions.enabled: false # 75% traffic</p><p><strong>Monitoring</strong>:</p><ul><li>Session operation metrics</li><li>Error rates comparison</li><li>Performance monitoring</li><li>User experience validation</li></ul><blockquote><strong>Instant Rollback</strong></blockquote><blockquote>Rollback can mean two things: (1) reverting the storage mode, or (2) reverting the deployed version. The preferred rollback is reverting to legacy mode using the same dual-mode binary (configuration change + rollout), which is immediate and does not require data migration. If we must revert the entire version, we roll back to a build that still supports both modes and run it in legacy mode. In both cases, because the two modes persist sessions differently, users with active sessions may need to re-authenticate, but the service remains available.</blockquote><h3>Dual-Mode Testing</h3><p>Every integration test must pass in both modes. The test infrastructure runs the full suite twice (controlled by environment variables), once in legacy mode, and once in separated mode. This continuous validation ensures both code paths remain functional throughout development.</p><h3>Performance Validation: Load Test Results</h3><p>Storing the sessions in a separate datastore is elegant in theory, but production performance is what really matters. Extensive load testing was conducted to validate the new architecture under realistic conditions. Fortunately, the results exceeded expectations!</p><h3>Test Environment</h3><p>For enterprise-scale validation (simulating some of our largest customers’ traffic patterns), a test environment was configured targeting <strong>7,500 logins/second:</strong></p><ul><li>The operational database was MongoDB Atlas M80 with 6 shards and 5 replicas across 3 regions.</li><li>The sessions database was a dedicated MongoDB Atlas M80 cluster with 6 shards and 3 replicas.</li><li>FlexID pods were scaled to 1,740 available cores.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/767/1*t_HC-srGsijULnpvYfGEHw.png" /><figcaption>Head-to-Head Comparison</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/766/1*_F6JRcY5ZZ1TiylVsjI5Kg.png" /></figure><h3>Key Findings</h3><p>The most striking result was the <strong>141ms reduction in p95 latency</strong> (from 392ms to 251ms). This improvement directly impacts user experience as authentication flows complete noticeably faster, reducing the perception of “waiting” during login. The improvement stems from eliminating monolithic document updates. In legacy mode, every session touch required loading and writing the entire user document. With separation, session operations hit a dedicated, optimized store.</p><p>The separated architecture achieved something the legacy approach couldn’t: <strong>load distribution across specialized stores</strong>. The operational database CPU dropped from 60% to 48%, while the sessions database handled its load at just 37% CPU. This headroom is critical for production stability as the legacy approach was pushing the operational database toward its limits, leaving no margin for traffic spikes.</p><p>With the separated architecture, throughput achieved was <strong>15,383 requests/second</strong> compared to 14,777 req/sec in legacy mode. A 4% throughput increase. More importantly, the system maintained this throughput with lower resource utilization, indicating significant additional headroom for growth.</p><h3>Results Across Database Backends</h3><p>In a different test, we also validated the architecture with different database combinations:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/765/1*cG5kZ6BLHBQftiwq1oFN5A.png" /></figure><p>The Redis-backed session store showed the best results for the operational database reducing MongoDB CPU utilization by <strong>40%</strong> (from 45% to 27%) while maintaining equivalent latency.</p><blockquote><strong>Production Implications</strong></blockquote><blockquote><strong>Cost efficiency:</strong> Lower CPU utilization means smaller database tiers can handle the same load.</blockquote><blockquote><strong>Reliability:</strong> 20% CPU headroom provides buffer for traffic spikes.</blockquote><blockquote><strong>Scale ceiling:</strong> Higher sustainable throughput delays the need for horizontal scaling.</blockquote><blockquote><strong>User experience:</strong> 36% faster p95 means fewer users experience slow authentications.</blockquote><p>The separated architecture positions the FlexID platform for independent scaling of each component and flexibility to evolve storage strategies over time. More importantly, the load test results validated that the architectural investment delivered measurable, production-relevant improvements (not just cleaner code), with <strong>36% faster authentication for end users of our customers</strong>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5c518dac2df5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/architecting-for-scale-session-separation-in-modern-authentication-platforms-5c518dac2df5">Architecting for Scale: Session Separation in Modern Authentication Platforms</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unveiling Face Recognition: From Pixels to Identity]]></title>
            <link>https://medium.com/transmit-security-engineering/unveiling-face-recognition-from-pixels-to-identity-11c800fdd279?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/11c800fdd279</guid>
            <category><![CDATA[user-identity-management]]></category>
            <category><![CDATA[face-recognition]]></category>
            <dc:creator><![CDATA[Gal Yona]]></dc:creator>
            <pubDate>Tue, 06 Jan 2026 17:24:04 GMT</pubDate>
            <atom:updated>2026-01-06T17:24:03.678Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://medium.com/u/eb029386590f">Gal Yona</a> &amp; <a href="https://medium.com/u/97fdbef77ef4">Ophir Yaniv</a></p><p>Engineering Team, Transmit Security</p><p><strong>This is the first post in a series on face recognition technology. In this post, we’ll explore the fundamentals, from how face recognition evolved to how modern systems actually work. Throughout this series, we’ll build a complete understanding of face recognition from theory to real-world implementation.</strong></p><p><strong>Face recognition is no longer futuristic. It’s part of our daily lives.</strong> From unlocking smartphones to passing through airport security, it quietly powers some of the most important moments in modern life. But how does it actually work? Behind the scenes is a fascinating blend of computer vision and AI.</p><p>In this blog post, we’ll break down key concepts that help reveal the true power and complexity of these systems and why understanding them matters in the identity and fraud prevention world.</p><h3>Understanding the Core of Face Recognition: Detection, Verification, and Identification</h3><p>Face recognition technology isn’t one-size-fits-all. At its core, it’s designed to answer three fundamentally different questions, each requiring distinct approaches and trade-offs. To grasp its full potential, it’s essential to break down the core components:</p><h3>1. Face Detection: Is there a face?</h3><p>This is the foundational step in any face recognition system. Detection identifies whether a face is present in an image or video and pinpoints its location using a bounding box. At this stage, there’s no recognition or identity, just the confirmation that a face exists.</p><h3>2. Face Verification (1:1): Are you who you say you are?</h3><p>Verification is a one-to-one comparison between two faces, typically between a live image and a stored biometric. It’s like matching a key to a specific lock. This method powers secure experiences like unlocking your phone or authorizing a high-value transaction in a banking app.</p><h3>3. Face Identification (1:N): Who is this?</h3><p>This is where the system compares one face to many — scanning a database to find a match. Identification enables powerful use cases like fraud detection, public security, and seamless customer recognition, even in complex environments like retail or transportation hubs.</p><h3>Real-World Applications: Where Face Recognition Delivers Value</h3><p>Face recognition delivers tangible value across industries, and its impact continues to grow. But not all face recognition systems are created equal. The priorities behind their design shift depending on the context of use.</p><ul><li><strong>Cooperative Systems: When the user opts in</strong></li></ul><p>In cooperative scenarios, the person being recognized is an active participant. These systems are designed for convenience and speed. Think of unlocking a phone with your face, accessing your laptop without typing a password, approving a payment in a banking app or standing at a biometric passport gate at airport border control. The engineering focus here is all about delivering a smooth user experience, protecting privacy, and ensuring security in milliseconds.</p><ul><li><strong>Non-Cooperative Systems: When recognition happens passively</strong></li></ul><p>These use cases involve recognizing individuals who may not be aware or engaged with the system. This includes surveillance cameras in public spaces, security monitoring at large venues, and law enforcement initiatives like identifying suspects from crowd footage. In these environments, performance hinges on high accuracy, scalability, and the ability to search vast databases under challenging conditions such as poor lighting or partial occlusion.</p><h3>Critical Challenges in Face Recognition</h3><p>Before exploring the technical pipeline, let’s examine the real-world challenges these systems face. As face recognition advances, so do concerns around security, fairness, and reliability.</p><ul><li><strong>Presentation attacks (Spoofing):</strong> Attackers attempt to trick the system using high-resolution photos, video replays, or even 3D masks. These attacks target the sensor level of the system.</li><li><strong>Defense:</strong> Liveness detection helps confirm a real person is in front of the camera. This can be done actively, with prompts like blinking or head movement, or passively, using texture analysis or depth sensing through 3D or infrared cameras.</li><li><strong>Adversarial attacks:</strong> These involve subtle changes, like digital noise or specially designed accessories such as adversarial glasses, that confuse the AI model without being obvious to the human eye.</li><li><strong>Defense:</strong> Robustness is improved through adversarial training, where models are exposed to these manipulations during training to learn how to resist them.</li><li><strong>Algorithmic Bias &amp; Fairness: </strong>Face recognition systems often show higher error rates for different gender, ethnic or age groups. Models trained on unbalanced datasets tend to underperform on underrepresented groups, which can lead to serious consequences such as false rejections or even wrongful arrests.</li><li>The answer is not just collecting more data but ensuring the training data is diverse and representative of the populations the system will serve.</li></ul><h3>The Face Recognition Pipeline: A Multi-Stage Journey</h3><h3><strong>Step 1: Face Detection — Finding Faces Fast</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/793/1*O6t6lj5VaHPyncuDBjoWXw.png" /><figcaption>High-level pipeline of modern face matching algorithms</figcaption></figure><p>Every face recognition journey starts with detection. The system must first locate all faces in an image or video.</p><ul><li><strong>Classical Methods: </strong>Lightweight techniques like Viola-Jones and HOG still run well on low-power devices but struggle with angled faces and varied lighting.</li><li><strong>AI-Powered Methods: </strong>Deep learning models like MTCNN, RetinaFace and YOLO-Face deliver high accuracy and speed. They detect faces in real time and can even map facial landmarks for downstream tasks.</li></ul><h3>Step 2: Face Alignment — Getting Faces Camera-Ready</h3><p>Once a face is detected, alignment ensures it’s properly positioned for accurate recognition. Real-world images vary in angle, lighting and scale, so alignment brings consistency.</p><p>The process involves:</p><ol><li><strong>Finding Facial Landmarks:</strong> Key points like the eyes, nose, and mouth are identified, often by the detection model itself (e.g., MTCNN).</li><li><strong>Applying Geometric Transformation:</strong> The face is rotated, scaled and shifted so features land in a standard position, typically with eyes level and centered.</li></ol><h3>Step 3: Feature Representation — Creating the “Embedding”</h3><p>This is often considered the “heart of the system.” A deep Convolutional Neural Network (CNN), trained on millions of faces, transforms the aligned image into a compact numerical vector called an “embedding.”</p><ul><li>Faces are turned into a compact vector of values, often 128 or 512 floating-point numbers, representing key facial features.</li><li>The model learns to focus on distinguishing traits like facial geometry, while ignoring less reliable ones like lighting or hair color.</li><li>Faces from the same person generate similar vectors, while faces from different people are far apart. Once this vector is created, the original image is no longer needed for matching.</li></ul><h3>Step 4: Matching and Decision — Comparing Embeddings</h3><p>The final stage involves simple mathematics: comparing embeddings and making a decision.</p><p><strong>Calculate Similarity:</strong> The distance between two embeddings is measured using a metric.</p><ul><li><strong>Euclidean Distance (L2):</strong> Measures the straight-line distance. A smaller distance indicates more similarity.</li><li><strong>Cosine Similarity:</strong> Measures the angle between vectors. A value closer to 1 indicates more similarity and is generally more robust to lighting changes.</li></ul><p><strong>Compare to a Threshold:</strong> The calculated similarity score is compared to a predefined threshold. If it passes, it’s considered a match.</p><p>The choice of this threshold is a critical business and product decision that balances security against convenience:</p><ul><li><strong>False Acceptance Rate (FAR):</strong> An imposter is incorrectly recognized. This is a security risk.</li><li><strong>False Rejection Rate (FRR):</strong> A valid user is incorrectly denied access. This impacts user experience.</li></ul><p>Finding the right balance depends heavily on the specific use case and system requirements.</p><h3>The Evolution of Face Representation: Key Milestones</h3><p>Before diving into how modern systems work, let’s trace the key breakthroughs that got us here. Each milestone fundamentally changed how computers ‘learn’ to recognize faces. Let’s explore three significant milestones in the evolution of how faces are represented:</p><h3>Classical Approach: Eigenfaces (1991)</h3><ul><li><strong>How it works:</strong> Uses Principal Component Analysis (PCA) to find the directions of greatest variance in a dataset of faces. These directions, when visualized as images, are the “eigenfaces.”</li><li><strong>Representation:</strong> Any face can be represented as a weighted sum of these eigenfaces, and the vector of weights becomes the feature vector.</li><li><strong>What stands out:</strong> The first truly automated, holistic method, proving the concept of a “face space.”</li><li><strong>Where it falls short:</strong> PCA is optimized for <em>reconstruction</em>, not <em>discrimination</em>. It finds commonalities rather than unique features, making it highly sensitive to lighting and pose variations.</li></ul><h3>Deep Learning Game-Changer: FaceNet &amp; Triplet Loss (2015)</h3><ul><li><strong>How it works:</strong> Trains a Convolutional Neural Network (CNN) using a “Triplet Loss” function.</li><li><strong>Training with Triplets:</strong> The network is fed three images at a time: an ‘Anchor’ face, a ‘Positive’ image of the same person, and a ‘Negative’ image of a different person.</li><li><strong>Loss Function Goal:</strong> To ensure that the distance between the Anchor and Positive images, plus a certain margin, is less than the distance between the Anchor and Negative images (dist(Anchor, Positive) + margin &lt; dist(Anchor, Negative)).</li><li><strong>What stands out:</strong> Directly optimizes the embedding space for discrimination, achieving record accuracy with compact embeddings.</li></ul><h3>ArcFace &amp; Angular Margin (2018)</h3><ul><li><strong>How it works:</strong> Modifies the standard classification loss (softmax) to enforce a larger angular margin between different identities.</li><li><strong>Geometric interpretation:</strong> All face embeddings are projected onto a high-dimensional hypersphere, and similarity is measured by the angle between vectors. During training, a margin is added to the angle (cos(θ + m)), making the classification task harder.</li><li><strong>Result:</strong> This forces features from the same person into tighter clusters (intra-class compactness) and pushes clusters of different people further apart (inter-class discrepancy).</li><li><strong>What stands out:</strong> Achieves better performance and is generally simpler and more stable to train than Triplet Loss.</li></ul><h3>The Bigger Picture</h3><p>Face recognition is undeniably powerful, but technical sophistication alone doesn’t guarantee success. Real-world effectiveness hinges on how well systems navigate fraud, edge cases, privacy, and fairness, often simultaneously. Each stage of the face recognition pipeline adds value, but also introduces risk if not designed thoughtfully.</p><p>That’s why it’s no longer enough to treat face recognition as a standalone tool. The real value comes from integrating it into a broader identity and fraud prevention strategy — one that is powered by AI, designed for scale, and capable of adapting in real time to both user behavior and emerging threats. As these technologies become more embedded in our daily lives, getting this balance right is critical for both security and trust.</p><p>In an upcoming post in this series, we’ll tackle one of the most critical challenges head-on. We’ll explore practical strategies for ensuring face recognition systems perform fairly across all demographics and work equitably for everyone.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ztT3hCevXlvo0N0b_hwCgA.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=11c800fdd279" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/unveiling-face-recognition-from-pixels-to-identity-11c800fdd279">Unveiling Face Recognition: From Pixels to Identity</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ARCH: The NextGen IaC Generator]]></title>
            <link>https://medium.com/transmit-security-engineering/arch-the-nextgen-iac-generator-60c2d2a980dc?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/60c2d2a980dc</guid>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[terragrunt]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[iac]]></category>
            <dc:creator><![CDATA[Elad Azary]]></dc:creator>
            <pubDate>Tue, 13 May 2025 09:28:28 GMT</pubDate>
            <atom:updated>2025-05-13T09:31:52.443Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*0NswdLHG3tosameh" /></figure><p>Let me tell you about the time we almost lost our minds trying to keep track of our cloud infrastructure at <strong>Transmit Security.</strong></p><p><strong><em>It started innocently enough.</em></strong> We had one product, a couple of environments, some AWS accounts, and a bit of GCP thrown in. <strong>Fast forward five years</strong>, and we were drowning in a sea of IaC configurations spread across dozens of product repositories with no clear standards.</p><p><em>“Which module version are we using in production again?”</em> became a daily question. <em>“Why does the staging environment have a different VPC setup than prod?”</em> was another favorite. And my personal nightmare: <em>“Why does this service have different permissions across different environments?”</em></p><p>Sound familiar? I thought so.</p><p>After having <strong>tons of conflicts</strong> that made us create new Cloud resources in several environments and import them to our state, we knew something had to give. That’s when we embarked on building <strong>ARCH</strong>, what I now consider our secret weapon. <br>Our <strong>Automated Reusable Configuration Hierarchy</strong> system, a comprehensive Infrastructure as Code management system that has completely transformed how we handle our Infrastructure code.</p><p>As a fast growing company with lots of products and environments, working on all major clouds, we needed a better solution for managing our infrastructure and scaling fast. Our identity and security solutions were expanding rapidly across AWS, GCP, and Microsoft Azure, and the traditional approach to infrastructure management <strong>simply couldn’t keep pace with our growth trajectory</strong>. We needed something that would eliminate bottlenecks, standardize deployments, and give our teams the agility to innovate without being bogged down by infrastructure complexities.</p><h3>The Infrastructure Management Challenge</h3><p>If you’ve worked with infrastructure at scale, you know the pain points:</p><ul><li>❌ <strong>Configuration drift</strong> between environments</li><li>🧠 <strong>Knowledge silos</strong> where only certain team members understand certain parts of the infrastructure</li><li>🔁 <strong>Inconsistent implementations</strong> of similar resources across different projects</li><li>🛠️ <strong>Tedious, error-prone manual processes</strong> for creating new environments or services</li></ul><p>Before our current system, our per product repositories were filled with different standards, inconsistent naming conventions, and a significant amount of duplicated and unorganized code. Making a simple change across all environments often meant <strong>updating dozens of files</strong>, with the constant risk of <strong>missing something important</strong>.</p><h3>Our Solution: A Hierarchical, Templated IaC System</h3><p>After evaluating various approaches, we designed a system that leverages Terragrunt and Gruntwork’s boilerplate to generate and maintain HCL files.</p><h4>Core Concepts</h4><ul><li>Define infrastructure values in a <strong>hierarchical and composable YAML structure</strong></li><li>Use <strong>templates</strong> to generate the actual HCL configuration files</li><li><strong>Automate</strong> everything with GitHub Actions</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/427/0*OVA8Zktyx78Wt-3s" /></figure><h3>The Building Blocks</h3><h4><strong>Central Repository</strong></h4><ul><li>HCL Templates: Organized by module and version</li><li>default.yaml &amp; globals.yaml: Common configs and latest module versions</li><li>Reusable workflow: A GitHub Action for rendering templates</li></ul><h4>Product-Specific Infrastructure Repository</h4><ul><li>Values: YAML files structured by <strong>product → cloud </strong>(AWS/GCP/Azure) <strong>→ environment </strong>(DEV/STG/PROD) <strong>→ region → project </strong>(Region’s main shared project, A specific customer’s project, etc.)</li></ul><p><strong>Teams work in their own repos</strong>, with ARCH generating the right HCL structure behind the scenes.</p><h3>Managing Terraform Module Compatibility with Version-Aware Templates</h3><p>Terraform modules evolve over time — sometimes with breaking changes that require different inputs, outputs, or structural definitions. To handle this without cluttering our logic or forcing “if-else” chaos in templates, we introduced <strong>version-aware HCL templates</strong>.</p><h4>Directory Layout Based on Module Major Version</h4><p>We structure our templates directory based on the <strong>major version</strong> of the Terraform module. Each versioned folder contains a complete, valid HCL template that maps to the expected structure of that module version.</p><pre>templates/<br>└── bigtable/<br>├── v0.x/<br>│ └── bigtable.hcl<br>└── v1.x/<br>  └── bigtable.hcl</pre><p>ARCH picks the <strong>right template</strong> automatically based on the module version in your YAML.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/427/0*jNnpZnbSjd3-UHty" /></figure><h4>How It Works</h4><p>Our YAML spec (the desired state) includes the desired <strong>module version</strong> (e.g., v1.2.3). During the rendering phase, the GitHub Action:</p><ol><li><strong><em>Parses the module version.</em></strong></li><li><strong><em>Extracts the major version</em></strong><em> </em>(e.g., v1.x from v1.2.3).</li><li><strong><em>Selects the correct template path</em></strong><em> </em>based on the major version.</li><li><strong><em>Renders the appropriate HCL</em></strong><em> </em>using the matching template and YAML input.</li></ol><p>This approach allows us to support multiple versions of the same module in parallel — critical when different environments or products are on different upgrade cycles.</p><h3>How It All Works Together</h3><p>The magic happens when we combine these components. Here’s the flow:</p><ol><li>🧩 Engineers update YAML values at the correct hierarchy level</li><li>🤖 GitHub Action merges configs (default → globals → product → cloud → environment → project → region)</li><li>🏗️ HCL templates are rendered using the final merged configuration</li><li>📤 Pull request with the generated HCL is created automatically</li></ol><p>This approach means that each service’s HCL file contains <strong>everything needed for deployment</strong>, making the process streamlined and less prone to errors.</p><h3>Why This Changed Everything</h3><p>This system has fundamentally changed how we manage infrastructure, delivering several key advantages:</p><h4>1. Simplified Configuration Management</h4><p>Define common settings once, override when needed.</p><p>Take a look at this folders tree:</p><pre>values/<br>├── default.yaml # Default Terraform module versions (Taken from the central repo)<br>├── globals.yaml # Organization-wide settings (Taken from the central repo)<br>├── product.yaml # Product-specific settings<br>├── &lt;cloud&gt;/<br>│ ├── cloud.yaml # Cloud-specific configurations<br>│ ├── &lt;environment&gt;/<br>│ │ ├── environment.yaml # Environment settings<br>│ │ ├── &lt;project&gt;/<br>│ │ │ ├── project.yaml # Project specific configuration<br>│ │ │ ├── regions/<br>│ │ │ │ ├── region.yaml # Region specific configuration<br>│ │ │ │ ├── &lt;region&gt;/<br>│ │ │ │ │ ├──values.yaml # Specific configuration</pre><p>This hierarchy allows us to make broad changes at higher levels while maintaining the flexibility to customize at more specific levels.</p><h4>2. Consistency Across All Environments</h4><p>Our pre-built templates ensure that all services are <strong>defined consistently</strong>, regardless of which team or engineer set them up. This consistency extends across environments, regions, and cloud providers.</p><p>For instance, a PostgreSQL database in development will have exactly the configuration structure it needs and will be as close as it can to the one in production, with only the specific values differing based on environment needs.</p><h4>3. Seamless Scaling</h4><p><strong>Want to provision a new environment or cloud?</strong> Just add a few YAML files.<br>The system takes care of the rest in minutes.</p><p>This scalability has been particularly valuable as we’ve expanded to new regions and added new products, allowing us to maintain a consistent infrastructure approach throughout our growth.</p><h4>4. Reduced Error Rates</h4><p>With automation handling the complex parts of infrastructure generation, we’ve seen a <strong>significant decrease in deployment errors</strong>. The system ensures that all dependencies are properly defined and that all required values are present before deployment.</p><h4>5. Centralized place for common configuration</h4><p>Need to whitelist new IPs of a vendor?<br>Add them <strong>once</strong>, and they propagate across all products.</p><h4>6. Eliminating infrastructure drifts</h4><p>To further reduce the risk of configuration drift and ensure our infrastructure remains up to date with our desired state, we implemented a periodic automation that executes the same infrastructure generation pipeline (generate-infrastructure action) for all relevant environments and opens a Pull Request only if there are changes.</p><h3>📍Real Example</h3><p>Let’s take a BigTable template for example, its template will look like that:</p><pre>terraform {<br>  source = &quot;&lt;TERRAFORM_MODULES_URL&gt;?version={{ .gcpBigtable.TFVersion }}&quot;<br>}<br><br>locals {<br>  {{- range .gcpLocals }}<br>  {{ .name }} = &quot;{{ .value }}&quot;<br>  {{- end }}<br>}<br><br>include &quot;root&quot; {<br>  path = find_in_parent_folders()<br>  expose = true<br>}<br><br>inputs = {<br>  project_id = {{ .gcpBigtable.project_id }}<br>  cluster_name = &quot;{{ .gcpBigtable.cluster_name }}&quot;<br>  zones_list = {{ indent 2 .gcpBigtable.zones_list }}<br>  region = &quot;{{ .gcpBigtable.region }}&quot;<br>  {{- if hasKey .gcpBigtable &quot;storage_type&quot; }}<br>  storage_type = &quot;{{ .gcpBigtable.storage_type }}&quot;<br>  {{- end }}<br>  {{- if hasKey .gcpBigtable &quot;autoscaling_parameters&quot; }}<br>  autoscaling_parameters = {{ indent 2 .gcpBigtable.autoscaling_parameters }}<br>  {{- end }}<br>  {{- if hasKey .gcpBigtable &quot;bigtable_members_list&quot; }}<br>  bigtable_members_list = {{ indent 2 .gcpBigtable.bigtable_members_list }}<br>  {{- end }}<br>  {{- if hasKey .gcpBigtable &quot;fixed_node_num&quot; }}<br>  fixed_node_num = {{ .gcpBigtable.fixed_node_num }}<br>  {{- end }}<br>  {{- if hasKey .gcpBigtable &quot;kms_rotation_period&quot; }}<br>  kms_rotation_period = {{ .gcpBigtable.kms_rotation_period }}<br>  {{- end }}<br>}</pre><p>When merging the yaml files in the hierarchy for product named “product”, in our “prod” environment “main” project in “gcp” cloud, located in “us-east1” region and getting file containing the following:</p><pre>globalLocals:<br>  productName: &quot;product&quot;<br>  environmentName: &quot;prod&quot;<br>  projectName: &quot;main&quot;<br>  regionName: &quot;us-east1&quot;<br><br>gcpBigtable:<br>  TFVersion: 1.2.3<br>  project_id: dependency.project.outputs.projects.project_id<br>  cluster_name: &quot;{{ .globalLocals.productName }}-{{ .globalLocals.environmentName }}-{{ .globalLocals.projectName }}-{{ .globalLocals.regionName }}&quot;<br>  region: &quot;{{ .globalLocals.regionName }}&quot;<br>  zones_list: |<br>    [<br>      &quot;{{ .globalLocals.regionName }}-a&quot;,<br>      &quot;{{ .globalLocals.regionName }}-b&quot;,<br>      &quot;{{ .globalLocals.regionName }}-c&quot;<br>    ]<br>  storage_type: &quot;SSD&quot;<br>  autoscaling_parameters: |<br>    {<br>      min_nodes_number = 4<br>      max_nodes_number = 500<br>      cpu_target_percent = 35<br>      storage_target_percent = 8192<br>    }</pre><p>The generated HCL file will look like this:</p><pre>terraform {<br>  source = &quot;&lt;TERRAFORM_MODULES_URL&gt;?version=1.2.3&quot;<br>}<br><br>locals {<br>  product_name = &quot;product&quot;<br>  cloud_name = &quot;gcp&quot;<br>  env_name = &quot;prod&quot;<br>  region_name = &quot;us-east1&quot;<br>  project_name = &quot;main&quot;<br>}<br><br>include &quot;root&quot; {<br>  path = find_in_parent_folders()<br>  expose = true<br>}<br><br>inputs = {<br>  project_id = dependency.project.outputs.projects.project_id<br>  cluster_name = &quot;product-prod-main-us-east1&quot;<br>  zones_list = [<br>    &quot;us-east1-a&quot;,<br>    &quot;us-east1-b&quot;,<br>    &quot;us-east1-c&quot;<br>  ]<br>  region = &quot;us-east1&quot;<br>  storage_type = &quot;SSD&quot;<br>  autoscaling_parameters = {<br>  min_nodes_number = 4<br>  max_nodes_number = 500<br>  cpu_target_percent = 35<br>  storage_target_percent = 8192<br>  }<br>}</pre><p>In all other environments that will use BigTable, the generated HCL will be built with the same standardization, that’s how we keep consistency while still preserving the flexibility of every environment to set its needed configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/426/0*4ARBagHZLk_hveov" /></figure><h3>✅ Real-World Impact</h3><p>We tested ARCH by <strong>migrating an old environment</strong> and <strong>creating a new one</strong>.</p><ul><li>Initial migration? Took time — but taught us how to structure everything.</li><li>Next environment? A <strong>single YAML file</strong> and we were done.</li></ul><h4>Results:</h4><ul><li>We<strong> cut the time</strong> for setting up infrastructure for new projects <strong>by roughly 85%</strong>. What used to take a working day can take couple of minutes if we configure things correctly.</li><li><strong>Configuration drift between environments?</strong> Pretty much eliminated. When we find a discrepancy, we fix it at the appropriate level in the hierarchy once, and boom — it propagates everywhere it needs to.</li><li>New team members are getting up to <strong>speed much faster</strong>. Previously, <strong>it would have taken a month</strong> just to understand our setup.</li><li>Our SecOps is much <strong>happier</strong> now that security configurations are applied consistently across all environments.</li></ul><h4>Bonus Benefits</h4><ul><li><strong>Team mobility</strong>. Now we can move engineers between teams without a massive ramp-up period. Since the infrastructure follows the same patterns across all products, a developer who’s been working on Product A can quickly contribute to Product B without learning a whole new infrastructure approach. This has been a game-changer for resource allocation.</li><li><strong>Automated module upgrades</strong>. We set up a periodic job that updates module versions in our hierarchy, generates the new configurations, and creates PRs. This means we can quickly roll out security patches or new features across our entire infrastructure with minimal manual work.</li><li>When a critical vulnerability was announced in one of our cloud providers recently, we were able to patch all affected resources across our entire organization in under a day. <strong>Before this system?</strong> That would have been weeks of work and plenty of missed instances.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/427/0*-wcZ0E9oZMQ24xDD" /></figure><h3>Lessons Learned &amp; Best Practices</h3><p>Building this system wasn’t without challenges. Here are some key lessons we’ve learned:</p><h4>The “Define Once” Philosophy</h4><p>We quickly learned that the single most important practice is our “<strong>define once</strong>” philosophy. Every value belongs at exactly one level in the hierarchy — the highest level where it makes sense.</p><p>Need to change a default timeout that affects all products? Change it in globals.yaml. <br>Need to update something specific to AWS in all environments? That goes to cloud.yaml file.</p><p>This approach means that when we need to make a change that affects multiple services or environments, we make it in one place. Next month, we’ll need to update our logging configuration across all products. <strong>One</strong> <strong>change</strong> to globals.yaml, <strong>one PR</strong>, and it will <strong>automatically</strong> be propagated everywhere. The old way? Probably 10+ PRs, change priority and context of members in every team and days of work.</p><h4>Self-Contained Definitions Save Sanity</h4><p>No more jumping through files to debug a resource.<br><strong>Everything is in one file</strong>.</p><h4>Automate Everything (Seriously, Everything)</h4><p><strong>Let automation be your best team member.</strong><br>Our GitHub Actions merge, render, validate, and open PRs — so we don’t have to.</p><h3>Conclusion</h3><p>I won’t pretend implementing this system was easy. We spent weeks designing the hierarchy, creating the initial templates, and building the automation. There were heated debates about where certain values belonged. And yes, there was pushback from teams comfortable with their existing approaches.</p><p>If you’re struggling with similar challenges, I’d encourage you to consider a hierarchical approach like ours. Start small, perhaps with just one product or environment, and gradually expand. Focus on building good templates and automation from the beginning. And remember that <strong>the goal isn’t just consistency for consistency’s sake — it’s about enabling your teams to move faster with confidence</strong>.</p><blockquote>The cloud infrastructure world isn’t getting any simpler. But with the right systems in place, it doesn’t have to be chaotic.</blockquote><p>________________________________________________________________<br><em>Feel free to drop questions in the comments if you’re tackling similar challenges.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=60c2d2a980dc" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/arch-the-nextgen-iac-generator-60c2d2a980dc">ARCH: The NextGen IaC Generator</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Chaos to Harmony: A Deep Dive into Centralizing Kubernetes Controller Upgrades]]></title>
            <link>https://medium.com/transmit-security-engineering/from-chaos-to-harmony-a-deep-dive-into-centralizing-controller-upgrades-dd57deb1cbec?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/dd57deb1cbec</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[argo-cd]]></category>
            <category><![CDATA[automation]]></category>
            <dc:creator><![CDATA[Elad Azary]]></dc:creator>
            <pubDate>Mon, 23 Sep 2024 07:46:43 GMT</pubDate>
            <atom:updated>2024-09-23T07:53:36.269Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iU3UPJ3LmKuMGSNbleZ8SA.png" /></figure><p><strong>TL;DR:</strong> At <a href="https://www.transmitsecurity.com/">Transmit Security</a>, we developed a centralized system for upgrading Kubernetes DevOps controllers across multiple products and environments. Our solution combines GitHub Actions, custom Helm charts, and automated processes to streamline upgrades, enhance security, and improve consistency. This article details our approach, its implementation, and its real-world impact on our operations.</p><p>_________________________________________________________________</p><p>In the fast-paced world of Kubernetes orchestration, staying current with controller versions is not just a best practice — it’s a necessity for maintaining robust, secure, and efficient infrastructure. Here at <a href="https://www.transmitsecurity.com/">Transmit Security</a>, where we manage a diverse portfolio of products across numerous Kubernetes clusters, this challenge is magnified by the scale and complexity of our operations. Today, we’re pulling back the curtain on an innovative approach we’ve developed to centralize and streamline the upgrade process for Kubernetes DevOps controllers across our multiple products and environments, a solution born from the unique demands of our robust infrastructure.</p><h3>The Challenge: Taming the Multi-Controller Beast</h3><p>Before we dive into our solution, let’s paint a picture of the challenges we faced:</p><ol><li><strong>Version Inconsistency</strong>: With multiple products and environments, keeping track of controller versions was like herding cats.</li><li><strong>Manual Overhead</strong>: Upgrades often required manual intervention, eating into valuable developer time.</li><li><strong>Lack of Standardization</strong>: Different teams had different upgrade processes, leading to potential errors and inconsistencies.</li><li><strong>Scale Issues</strong>: As our infrastructure grew, so did the complexity of managing upgrades across the board.</li><li><strong>Security Concerns</strong>: Delayed upgrades could potentially expose our systems to known vulnerabilities.</li></ol><p>We needed a solution that would address these challenges while providing flexibility and control to our DevOps teams. Enter our centralized upgrade pipeline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*4sntUqGJ2J_G9TDe" /><figcaption>Managing Kubernetes assets can be so frustrating…</figcaption></figure><h3>Our Solution: A Symphony of Automation and Control</h3><p>We’ve architected a comprehensive solution that harmonizes GitHub Actions, custom Helm charts, and automated processes into a streamlined upgrade symphony. Let’s break down the key components and processes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*EBZwI81WTF9hpmV6" /></figure><h3>Key Components: The Building Blocks</h3><h4>Charts</h4><p><strong>Devops-stack-charts</strong>: This is our maestro chart, conducting the orchestra of external charts. It contains:</p><ul><li>External charts (such as cert-manager, cluster-autoscaler, etc.) defined in the Chart.yaml</li><li>A template file for each controller in the templates directory</li><li>Custom resources for each controller’s chart</li></ul><p>This elegant structure empowers us to efficiently manage and customize resources for each controller while maintaining a centralized approach, significantly enhancing our operational efficiency.</p><p><strong>Application-sets-chart</strong>: Think of this as our sheet music. It contains:</p><ul><li>A template for a <a href="https://argo-cd.readthedocs.io/en/stable/user-guide/multiple_sources/">MultiSource</a> ApplicationSet resource</li><li>Per-product values files with ApplicationSet configurations</li><li>Global defaults and a hierarchy (product_name &gt; cloud_name &gt; environment_name)</li><li>Version specifications for controllers</li><li>Git repository URLs and branches for controller-specific values</li><li>Sync options for the ApplicationSet</li></ul><p>This chart is instrumental in our ability to manage configurations consistently across diverse products and environments, ensuring uniformity and reliability in our deployments.</p><h4>GitHub Actions: The Automation Ensemble</h4><p><strong>renovate-scheduled.yaml</strong>: Our update scout. Running periodically, it:</p><ul><li>Checks for new controller versions</li><li>Triggers the publish-chart action when updates are found</li></ul><p>This proactive approach ensures that our system consistently benefits from the latest controller versions, enhancing security and performance.</p><p><strong>publish-chart.yaml</strong>: The Packaging and Publishing Maestro. It:</p><ul><li>Packages each chart from external helm repositories (e.g., Bitnami, Jetstack)</li><li>Incorporates relevant custom resources from devops-stack-charts/templates</li><li>Publishes the resulting chart to our internal artifact repository</li></ul><p>This process ensures that our customized charts are readily available for deployment, streamlining our infrastructure management.</p><p><strong>generate-applicationset.yaml</strong>: Our template virtuoso. This action:</p><ul><li>Generates ApplicationSet templates based on user parameters</li><li>Creates a new branch</li><li>Opens a PR in the specific product’s repository (e.g., mind-infra, riskid-infra)</li></ul><p>By ensuring that new configurations undergo review before implementation, it maintains the integrity of our infrastructure.<br>This action triggers a script called get_target_repo.py that contains the mapping of a product name &lt;&gt; repository name and helps us understand to which repository should we open the PR.</p><p><strong>apply-appset.yaml</strong>: This action is our deployment conductor. It:</p><ul><li>Runs in per-product repositories</li><li>Applies an ApplicationSet based on user-provided parameters</li></ul><p>Its role is pivotal in the bootstrapping process, as the ApplicationSet takes over the reins for automatic syncing post-deployment.</p><pre>├──README.md<br>├──.github<br>│ ├── scripts<br>│   └── get_target_repo.py<br>│ └── workflows<br>│ │  ├── apply-appset.yaml<br>│ │  ├── generate-applicationset.yaml<br>│ │  ├── publish-chart.yaml<br>│ │  └── renovate-scheduled.yaml<br>├── application-sets-chart<br>│ ├── Chart.yaml<br>│ ├── values.yaml<br>│ ├── productA.values.yaml<br>│ ├── productB.values.yaml<br>│ └── templates<br>│   └── applicationset.yaml<br>├── devops-stack-charts<br>│ │  ├── Chart.yaml<br>│ │  ├── templates<br>│ │  │ ├── controllerA.yaml<br>│ │  │ └── controllerB.yaml<br>│ └── values.yaml<br>└── renovate.json</pre><h3>The Upgrade Process: A Well-Orchestrated Performance</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ialei_6maXCdv4X9bl0zmQ.gif" /></figure><ol><li><a href="https://docs.renovatebot.com/">Renovate</a> Scheduler checks for updates</li><li>If new versions found, trigger Publish Chart Action</li><li>Pull the changed Chart</li><li>Add Custom Resources</li><li>Package the updated Chart</li><li>Publish to the artifact repository</li><li>Create PRs in Dev Values</li><li>Generate ApplicationSet</li><li>Create PRs in Product Repository</li><li>DevOps Team Review</li><li>If approved, Deploy to Dev</li><li>Test in Dev</li><li>If successful, Update Other Environments</li><li>Generate New ApplicationSets</li><li>Deploy to Staging/Prod</li></ol><p>Let’s walk through this process:</p><ol><li><strong>Weekly Update Check:</strong> <br>Periodically, our Renovate scheduler GitHub Action runs its reconnaissance mission, scouting for controller updates. This regular check ensures we’re always aware of the latest versions available.</li><li><strong>Automated Chart Publishing:</strong> <br>When Renovate detects updates, it triggers our publish-chart action. This process efficiently retrieves all charts from their repositories, unarchives them, and integrates our custom resources for each controller. The charts are then repackaged with these enhancements and published to our artifact repository. This streamlined approach ensures our deployment pipeline always has access to up-to-date, customized charts, ready for immediate use. By automating this process, we maintain consistency, reduce errors, and significantly improve our deployment efficiency.</li><li><strong>Automated PR Creation — The Proposal Stage:</strong> <br>As soon as new controller versions hit the artifact repository, our system kicks into high gear:<br>Pull Requests are automatically created in each per-product values file, specifically in the development section within the application-sets-chart in our devops-stack repository. The generate-applicationset action runs for each product’s development environment, preparing the ground for potential upgrades. PRs pop up in per-product repositories, suggesting changes to the ApplicationSets for development environments. <br>This automated PR creation serves as a proposal system, allowing teams to review changes before implementation.</li><li><strong>DevOps Team Review — The Human Touch:</strong> <br>While automation is at the heart of our system, we believe in the importance of human oversight:<br>DevOps teams receive Slack notifications about new versions, keeping them in the loop. Teams review the proposed changes, considering factors like compatibility and potential impact. They make the call on whether to implement the new version in the development environment. This step ensures that despite the automation, humans remain in control of what goes into their systems.</li><li><strong>Controlled Rollout: From Dev to Production</strong> After a successful deployment in the development environment, DevOps teams can confidently roll out the new version across other environments: <br>They modify their per-product values file in the application-sets-chart (located in the DevOps-Stack repository), updating the version for the desired environments. Using the generate-applicationset GitHub action, they trigger the generation of new PRs for each cluster that needs the update. This process allows for a gradual, controlled rollout, enabling teams to monitor the impact at each stage.</li></ol><h3>ArgoCD: One Instance Per Kubernetes Cluster</h3><p>A key aspect of our architecture is running a dedicated ArgoCD instance for each Kubernetes cluster. This approach offers several advantages:</p><ol><li><strong>Isolation</strong>: Each cluster’s configuration and state are isolated, reducing the risk of cross-cluster issues.</li><li><strong>Performance</strong>: Dedicated ArgoCD instances ensure optimal performance for each cluster, preventing resource contention.</li><li><strong>Scalability</strong>: As we add new clusters, we can easily scale our ArgoCD deployment without impacting existing clusters.</li><li><strong>Security</strong>: Cluster-specific ArgoCD instances enhance security by limiting the blast radius of potential breaches.</li><li><strong>Customization</strong>: We can tailor ArgoCD configurations to the specific needs of each cluster.</li><li><strong>Resilience</strong>: Issues with one ArgoCD instance don’t affect other clusters, improving overall system resilience.</li></ol><p>This architecture aligns perfectly with our centralized controller upgrade strategy, allowing for granular control and monitoring of the upgrade process across our infrastructure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/984/1*54DITUwO-V9NwM9DwXZQyw.gif" /></figure><h3>Per-Product Repository Structure</h3><p>To give you a clearer picture of how our solution is organized, let’s take a look at the structure of our per-product repositories:</p><pre>product-infra/<br>├── devops-stack/<br>│ ├── appsets/<br>│ │ ├── aws/<br>│ │ │ ├── dev/<br>│ │ │ │ ├── cluster-name/<br>│ │ │ │ │ └── applicationSet.yaml<br>│ │ │ │ └── another-cluster/<br>│ │ │ │ └── applicationSet.yaml<br>│ │ │ ├── staging/<br>│ │ │ └── prod/<br>│ │ └── gcp/<br>│ │ │ ├── dev/<br>│ │ │ ├── staging/<br>│ │ │ └── prod/<br>│ └── apps/<br>│ ├── aws/<br>│ │ ├── values.yaml<br>│ │ ├── dev/<br>│ │ │ ├── values.yaml<br>│ │ │ ├── cluster-name/<br>│ │ │ │ └── values.yaml<br>│ │ │ └── another-cluster/<br>│ │ │ └── values.yaml<br>│ │ ├── staging/<br>│ │ └── prod/<br>│ └── gcp/<br>│ │ ├── dev/<br>│ │ ├── staging/<br>│ │ └── prod/<br>└── README.md</pre><p>This structure allows us to:</p><ol><li>Organize resources by cloud provider (aws, gcp)</li><li>Separate environments (dev, staging, prod)</li><li>Manage multiple clusters within each environment</li><li>Keep product-specific resources separate from the DevOps stack</li><li>Maintain consistency across products while allowing for product-specific customizations</li></ol><p>The applicationSet.yaml files in each cluster directory are the key to our centralized upgrade process, as they define which controller versions and configurations to apply to each cluster.</p><h3>Benefits: Why This Approach is a Game-Changer</h3><ol><li><strong>Centralized Management</strong>:</li></ol><ul><li>Single source of truth for controller versions and configurations</li><li>Easier auditing and compliance management</li><li>Reduced cognitive load on DevOps teams</li></ul><p><strong>2. Automated Updates</strong>:</p><ul><li>Significant reduction in manual intervention</li><li>Faster response to critical updates and security patches</li><li>Reduced human error in the update process</li></ul><p><strong>3. Consistency Across the Board</strong>:</p><ul><li>Uniform controller versions across products and environments</li><li>Standardized upgrade process for all teams</li><li>Easier troubleshooting due to version consistency</li></ul><p><strong>4. Controlled Rollout</strong>:</p><ul><li>DevOps teams retain oversight and control</li><li>Ability to test updates in development before production rollout</li><li>Flexible upgrade schedules per product and environment</li></ul><p><strong>5. Scalability</strong>:</p><ul><li>Easily adaptable to new products and controllers</li><li>Process remains efficient regardless of the number of clusters or controllers</li><li>Reduced overhead as infrastructure grows</li></ul><p><strong>6. Enhanced Security</strong>:</p><ul><li>Faster implementation of security patches</li><li>Reduced window of vulnerability due to outdated controllers</li><li>Improved compliance with security best practices</li></ul><p><strong>7. Time and Resource Efficiency</strong>:</p><ul><li>DevOps teams can focus on strategic tasks rather than manual upgrades</li><li>Reduced downtime and smoother upgrades</li><li>Cost savings from optimized use of human resources</li></ul><h3>Real-world Impact: A Case Study</h3><p>To illustrate the power of this approach, let’s look at the following scenario:</p><p>When a critical security patch was released for one of our key controllers, our Renovate action picked it up. The publish-chart action immediately packaged and published the updated chart to the artifact repository. PRs were automatically created for our development environments across all products.</p><p>Our DevOps teams, alerted by Slack notifications, quickly reviewed and approved the changes. Within a day, the patch was tested and deployed across all development environments. Using the controlled rollout process, teams then systematically updated staging and production environments over the next maintenance time.</p><p>What once would have been a long, stress-filled scramble to manually update controllers across our entire infrastructure was reduced to a smooth process with minimal manual intervention.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fBt-wa9xGYXWZBmKO_vaBw.jpeg" /></figure><h3>Conclusion: Embracing the Future of Kubernetes Management</h3><p>By centralizing our Kubernetes DevOps controller upgrade process, we’ve not only streamlined our operations but also set the stage for more advanced automation and optimization in our Kubernetes ecosystem. This approach has transformed a once-cumbersome process into a streamlined, efficient operation that enhances our ability to maintain a cutting-edge, secure infrastructure.</p><p>As the Kubernetes landscape continues to evolve, so too will our processes and tools. By building a flexible, automated foundation, we’re well-positioned to adapt to whatever challenges the future of container orchestration may bring.</p><p>I’m excited about the possibilities this centralized approach opens up and committed to continual refinement and innovation in our DevOps practices.</p><p>_________________________________________________________________</p><p><em>I’d love to hear from you! Share your experiences, questions, or insights in the comments below and feel free to reach me here or via my</em><a href="https://www.linkedin.com/in/elad-azary-b828a3ba/"><em> LinkedIn</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dd57deb1cbec" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/from-chaos-to-harmony-a-deep-dive-into-centralizing-controller-upgrades-dd57deb1cbec">From Chaos to Harmony: A Deep Dive into Centralizing Kubernetes Controller Upgrades</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building ArgoCD Ecosystem With Secret Management — The GitOps Way (Part II)]]></title>
            <link>https://medium.com/transmit-security-engineering/building-argocd-ecosystem-with-secret-management-the-gitops-way-part-ii-683ed72d2028?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/683ed72d2028</guid>
            <category><![CDATA[vault]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[argo-cd]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[infrastructure]]></category>
            <dc:creator><![CDATA[Ziv Vilozni]]></dc:creator>
            <pubDate>Mon, 14 Nov 2022 12:01:51 GMT</pubDate>
            <atom:updated>2022-11-14T12:01:51.001Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building ArgoCD Ecosystem With Secret Management — The GitOps Way (Part II)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*izDTCRb1VsL3WGaU2SIzuQ.png" /></figure><h4>The secret tool research</h4><p>Once we’ve deployed our <a href="https://argo-cd.readthedocs.io/en/stable/">ArgoCD</a> servers in IaC way, in order to start using GitOps approach in our Kubernetes products with multiple environments (development, staging and production) it was the right time to handle the Secrets management challenge.</p><p>The legacy deployments in our products used the CI tool (Jenkins) with Jenkins credentials in order to push secrets to the helm values files or just manual creation (bad idea, we know that 🙂), so in order to create our Kubernetes charts on-top our new ArgoCD servers, we came to an understanding that we had to change our secrets management.</p><p>First step in solving the challenge would be — defining the problem and the requirements:</p><ul><li>Easy integration with ArgoCD</li><li>Support <a href="https://www.vaultproject.io/">HashiCorp Vault</a></li><li>Working with all Kubernetes resources (some of our application charts includes secrets inside helpers and config maps)</li><li>Integration with Helm</li></ul><p>After a market research we’ve conducted, based on our requirements, we came to a conclusion that our choice is between <a href="https://argocd-vault-plugin.readthedocs.io/en/stable/">Argo CD Vault Plugin</a> and <a href="https://external-secrets.io/v0.6.1/">External Secrets Operator</a>.</p><p>Because of ArgoCD Vault plugin being part of the ArgoCD ecosystem, we’ve chosen to begin our PoC with it.</p><h4>How the Argo CD Vault Plugin being installed</h4><p>Using Terraform value templating we’ve demonstrated in <a href="https://medium.com/transmit-security-engineering/building-argocd-ecosystem-with-secret-management-the-gitops-way-part-i-49b4921f7c1f">part 1</a>, we can add vault plugin built-in integration, first creating a secret with vault authentication credentials with <a href="https://argocd-vault-plugin.readthedocs.io/en/stable/config/">chosen authentication method</a>, you can find the example <a href="https://github.com/Transmit-Security-Engineering/argocd-with-secrets-mgmt/blob/main/terraform/secret.tf">here</a>.</p><p>Adding it to the ArgoCD configuration template file using the following <a href="https://github.com/Transmit-Security-Engineering/argocd-with-secrets-mgmt/blob/main/terraform/config_template.yaml">configuration</a>.</p><h4>How the Argo CD Vault Plugin Works</h4><p>The Argo CD Vault Plugin works by replacing the pattern of &lt;placeholder&gt; in any kubernetes YAML or JSON manifests with the secrets values taken from Vault.</p><p>An annotation can be used to specify exactly the path that the plugin should look the Vault values. The annotation needs to be in the following format:</p><pre>kind: Secret<br>apiVersion: v1<br>metadata:<br>   name: secrect-with-annotation<br>   annotations:     <br>      avp.kubernetes.io/path: &quot;path/to/password&quot;<br>type: Opaque<br>stringData:  <br>  password: &lt;password&gt;</pre><p>Another way to specify the Vault secret path is using inline-path placeholders. This way allows us to specify path and key for multiple distinct secrets into the same YAML:</p><pre>kind: Secret<br>apiVersion: v1<br>metadata:<br>  name: secrect-without-annotation<br>type: Opaque<br>stringData:<br>  username: &lt;path:some/path/to/usernames#secret-key&gt;<br>  password: &lt;path:some/path/to/passwords#secret-key&gt;</pre><p>Once the plugin has finished replacing the placeholders with the secrets, ArgoCD applies the updated manifest to the Kubernetes cluster.</p><h4>Prerequisites</h4><p>In this tutorial we wont cover how to install Vault server, but you can find <a href="https://www.vaultproject.io/docs/platform/k8s/helm/run">here</a> the Vault installation documentation.</p><h4>Deploy an ArgoCD application to development environment using the ArgoCD Vault Plugin with helm values file</h4><p>First thing, we need to enable a Vault kv-v2 store:</p><pre>$ vault secrets enable -version=2 argocd</pre><p>Then, we need to create the kv secrets in vault for development environment.</p><p>Development secret:</p><pre>$ vault kv put argocd/development username=my_dev_user password=my_dev_pass</pre><pre>===== Secret Path =====<br>argocd/data/development</pre><pre>======= Metadata =======<br>Key                Value<br>---                -----<br>created_time       2022-10-31T20:46:20.138968925Z<br>custom_metadata    &lt;nil&gt;<br>deletion_time      n/a<br>destroyed          false<br>version            1</pre><p>Global secret:</p><pre>$ vault kv put argocd/global globalToken=Mytoken</pre><pre>=== Secret Path ===<br>argocd/data/global</pre><pre>======= Metadata =======<br>Key                Value<br>---                -----<br>created_time       2022-10-31T21:03:07.745292476Z<br>custom_metadata    &lt;nil&gt;<br>deletion_time      n/a<br>destroyed          false<br>version            1</pre><p>Now, let’s go back to our example of the helm chart application, which containing generic nginx with two templated secret manifests, env_secret.yaml and inline_path_secret.yaml. You can find this chart example on GitHub <a href="https://github.com/Transmit-Security-Engineering/argocd-with-secrets-mgmt"><strong>repository</strong></a> under the helm directory.</p><p>It’s<strong> </strong>worth paying attention to the templates secrets manifests, <strong>that represent</strong> <strong>two different ways of plugin usage.</strong></p><p>annotation usage (env_secret.yaml):</p><pre>apiVersion: v1<br>kind: Secret<br>metadata:<br>  name: my-secret-example<br>  annotations:<br>    avp.kubernetes.io/path: &quot;argocd/data/{{ .Values.env }}&quot;<br>type: Opaque<br>stringData:<br>  password: &lt;password&gt;</pre><p>inline-path usage (inline_path_secret.yaml):</p><pre>kind: Secret<br>apiVersion: v1<br>metadata:<br>  name: secrect-without-annotation<br>type: Opaque<br>stringData:<br>  username: &lt;path:argocd/data/{{ .Values.env }}#username&gt;<br>  token: &lt;path:argocd/data/global#globalToken&gt;</pre><p>Finally, we need to create the ArgoCD application for development environment.</p><p>Can be saved as <strong>argo_app_dev.yaml</strong>:</p><pre>apiVersion: argoproj.io/v1alpha1<br>kind: Application<br>metadata:<br>  name: argo-dev-app<br>  namespace: argocd # The namespace must match the namespace of your Argo CD instance - typically this is argocd.</pre><pre>finalizers:<br>  - resources-finalizer.argocd.argoproj.io<br>spec:<br>  destination:<br>    namespace: default # application namespace<br>    server: <a href="https://kubernetes.default.svc/">https://kubernetes.default.svc</a><br>  project: default # argo project<br>  source:<br>    path: helm # Path to appsPath with the template name<br>    repoURL: <a href="/service/mailto:git@github.com">git@github.com</a>:Transmit-Security-Engineering/argocd-with-secrets-mgmt.git # our repo with the chart example<br>    targetRevision: main<br>    plugin:<br>      name: argocd-vault-plugin-helm-with-args<br>      env:<br>        - name: HELM_ARGS<br>          value: -f dev.values.yaml # here we configured the values file of our environment<br>  syncPolicy:<br>    syncOptions:<br>      - CreateNamespace=true<br>    automated:<br>      prune: true<br>      selfHeal: true<br>---</pre><p>Create the ArgoCD application with kubectl:</p><pre>$ kubectl create -f argo_app_dev.yaml</pre><p>Time to check if we can see the application in the ArgoCD UI:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Kl3COcpeYUvZn2K5hSLOKg.png" /></figure><p>We can validate our secrets values using the following commands:</p><pre>$ kubectl get secret my-secret-example -o jsonpath=&quot;{.data.password}&quot; | base64 --decode<br>my_dev_pass%</pre><pre>$ kubectl get secret secrect-without-annotation -o jsonpath=&quot;{.data.token}&quot; | base64 --decode<br>Mytoken%</pre><pre>$ kubectl get secret secrect-without-annotation -o jsonpath=&quot;{.data.username}&quot; | base64 --decode<br>my_dev_user%</pre><h4>Changing the Vault Secret</h4><p>What about the support of changing/rotating the secrets?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5EJQt5eHntFzIitz" /><figcaption>Photo by <a href="https://unsplash.com/@earbiscuits?utm_source=medium&amp;utm_medium=referral">Juan Rumimpunu</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Time to figure out how it works!</p><p>First, update our development password secret in vault:</p><pre>vault kv put argocd/development password=my_new_dev_pass</pre><pre>===== Secret Path =====<br>argocd/data/development</pre><pre>======= Metadata =======<br>Key                Value<br>---                -----<br>created_time       2022-11-01T00:20:08.127676478Z<br>custom_metadata    &lt;nil&gt;<br>deletion_time      n/a<br>destroyed          false<br>version            2</pre><p>Performing a manual hard refresh to the application via the ArgoCD UI, will make a dry run of the plugin:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/276/1*iLFYyqKbEzc60eNuBPQAkA.png" /></figure><p>Then, the ArgoCD Vault Plugin will update the secret from Vault. ArgoCD will perform a sync to the application and will update the Kubernetes secret.</p><p>And we’re good to go:</p><pre>$ kubectl get secret my-secret-example -o jsonpath=&quot;{.data.password}&quot; | base64 --decode<br>my_new_dev_pass%</pre><p>So it’s time to make a decision.</p><p>In the beginning, the implementation of the ArgoCD Vault Plugin works great for us. The plugin support with all Kubernetes resources was the best feet for our helm application structure. In addition, the plugin did not require an Operator or Custom Resource to manage and maintain.</p><p>But then, we ran into some issues…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4EbR4_7hphTIvGRa" /><figcaption>Photo by <a href="https://unsplash.com/@neonbrand?utm_source=medium&amp;utm_medium=referral">Kenny Eliason</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>The problem started when some of our developers teams requested an auto sync secrets support.</p><p>Unfortunately, sync secrets in the plugin working in manual way or can be implement with custom webhook/CronJob (auto refresh of secrets probably won’t be included in the next Argo CD Vault Plugin versions —<a href="https://github.com/argoproj-labs/argocd-vault-plugin/issues/224">reference to the GitHub issue</a>).</p><p>The sync disadvantage and the fact that the plugin can be configured to work with only one secret manager at time, brought us to start testing the second option, the<strong> External Secrets Operator</strong>.</p><p>In this post, you have learned how to deploy an helm chart with values file on ArgoCD server, taking the secrets from Vault and using the Argo CD Vault Plugin as our secret management tool.</p><p>On our next blog, we would like to focus on External Secrets Operator with ArgoCD integration — so make sure you read the third part!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=683ed72d2028" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/building-argocd-ecosystem-with-secret-management-the-gitops-way-part-ii-683ed72d2028">Building ArgoCD Ecosystem With Secret Management — The GitOps Way (Part II)</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OAS — The SaaS Provider Superpowers]]></title>
            <link>https://medium.com/transmit-security-engineering/oas-the-saas-provider-superpowers-5f532cad8203?source=rss----262939e6cd0e---4</link>
            <guid isPermaLink="false">https://medium.com/p/5f532cad8203</guid>
            <category><![CDATA[transformation]]></category>
            <category><![CDATA[saas]]></category>
            <dc:creator><![CDATA[Orit Yaron]]></dc:creator>
            <pubDate>Wed, 09 Nov 2022 07:26:26 GMT</pubDate>
            <atom:updated>2022-11-09T07:26:26.212Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>OAS — The SaaS Provider Superpowers</strong></h3><p>Digital transformation is on, full power!, and with that more organizations turn to either leveraging SaaS solutions, or migrating their own capabilities to a SaaS model.</p><p>What differentiates an OK SaaS provider from a great one? We refer to those as the OAS of SaaS, the superpowers of a great SaaS provider. When you have it — you are Superman. When you don’t, you are Clark Kent holding kryptonite…</p><p>So, what is OAS? OAS stands for Observability, Security and Availability. The three pillars for achieving a great SaaS operation with common denominator — in order to achieve excellence in any of those you would need to implement a layered approach solution.</p><p>On this blog we will touch on what each of those mean, whereas in following posts we will share in more detail how we implement each of those in Transmit Security.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-tnJC7lTH1IJjdKk" /><figcaption><strong>Photo by </strong><a href="https://unsplash.com/@messrro?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText"><strong>Mehdi MeSSrro</strong></a><strong> on </strong><a href="https://unsplash.com/s/photos/superman?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText"><strong>Unsplash</strong></a></figcaption></figure><p><strong>O</strong>bservability</p><p>Every organization, and especially a SaaS one, would like to be in a position of making decisions based on data. This will work well, provided:</p><ol><li>You have good quality data</li><li>Your data is well organized in a way that will allow you to benefit knowledge and insights</li></ol><p>Your Observability stack is a critical part of your immune system — much like in Biology, this immune system needs to be able to protect you by blocking / preventing, and alert you and notify you <strong>before</strong> things go really bad, so you will have time to react and implement the right medicine.</p><p>In a nutshell the Observability stack will consist of 3 types of systems:</p><ul><li>Data gathering technologies — collecting and storing metrics, logs etc.</li><li>Presentation technologies — allowing you to arrange the data in a human easy to understand dashboards</li><li>Alerting technologies — enabling you to get push alerts based on predefined thresholds and scenarios</li></ul><p><strong>A</strong>vailability</p><p>If your service isn’t available, it is not generating any value. In a global world that operates 24x7 there is no room for a service that is not meeting the same level of availability.</p><p>The SaaS availability is driven by many underlying components, and eventually will be determined by the weakest link in the chain. To name a few: Compute power, Network infrastructure, Code resilience etc.</p><p>To assure you meet the desired availability, you would need to implement a layered approach:</p><p>First introducing <strong>Redundancy</strong> of your components (clustering your compute resources, leveraging more than one provider, traffic multi path, geographic spread)</p><p>Second in line is <strong>Recovery</strong>, protecting your service in case of failure such as data corruption, where keeping multiple synced copies of data will not save the day</p><p>Third layer is <strong>Testing</strong>. The redundancy and recovery operations implemented must be tested (and monitored) on an on-going basis — or you will find that they failed at the worst time ever. The most common examples for glorious failures, is a backup process with no restore test, or having a DR site which never received traffic and was left behind.</p><p>When you are skipping the third layer of Testing you are losing twice — on one hand you pay for overhead of having those in place, on the other hand, when you actually need it — it is useless.</p><p><strong>S</strong>ecurity</p><p>As part of the increasing digital transformation, we see more audiences acquiring and using online services. As a result we have more identities available online, and more fraud opportunities. And Indeed we see the increase in account take over (ATO) attacks.</p><p>Providing a service is a big responsibility. Protecting user information is even bigger, so one should not take it lightly, especially in light of the increasing threats.</p><p>To achieve your SaaS Security, as in any of the other OAS superpowers, you would need to implement a layered approach, starting with the first line of code you run.</p><p>Code security started with secured design, going through implementation, for example code scanning and vulnerability detection as part of the development process. Whether you choose to follow OWASP or other guidelines, the important part is not to ignore the findings and when needed prioritize fixes.</p><p>Second in line is the infrastructure security. Same here, regardless of the CSPM tool you are using — don’t underestimate the findings.</p><p>Third layer is the perimeter protection you should consider. Many different technologies should be considered here, to name a few: WAF (which should also provide you with DDoS protection), VPN (Keep inside what does not need to be outside…)</p><p>Now all you have to do is to go and build your OAS. Luckily, unlike Superman, it is not a trade acquired by birth, but capabilities you can build and develop, like a muscle.</p><p>It will require hard work and dedication — but this is also part of the fun in doing it. In coming posts we will share how we build our OAS SaaS muscles and powers — a never ending journey of continuance development and improvement.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5f532cad8203" width="1" height="1" alt=""><hr><p><a href="https://medium.com/transmit-security-engineering/oas-the-saas-provider-superpowers-5f532cad8203">OAS — The SaaS Provider Superpowers</a> was originally published in <a href="https://medium.com/transmit-security-engineering">Transmit Security Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>