Dynamische Kontaktformulare für statische Webseiten
Im Zeitalter der Cloud ist das Hosten einer statischen Website billiger, schneller und einfacher als das herkömmliche On-Premise Hosting, bei dem immer mindestens ein Server laufen und gewartet werden muss.
AWS bietet eine sehr einfache und kostengünstige Möglichkeit, statische Webseiten in einem S3-Bucket unter der eigenen Domain zu hosten. Man kann entweder eine neue Domain bei AWS Route 53 registrieren oder die eigene Domain auf Route 53 übertragen.
Und natürlich möchte man keinen Server hochfahren, um ein einfaches Kontaktformular auf seiner Webseite integrieren zu können. Allerdings ist das Kontaktformular trotzdem ein sehr kristisches Element auf der Seite, da dies eben auch nicht als Einfallstor für Spam-Bots dienen soll.
Glücklicherweise bietet AWS eine einfache Möglichkeit, dies komplett serverless umzusetzen. Wir können AWS Lambda mit Amazon API Gateway verwenden, um ein serverloses Backend zu erstellen und mit dem Amazon Simple Email Service (SES) eine E-Mail an das Unternehmen zu senden, wenn ein Kunde eine Anfrage oder ein Feedback sendet. Schauen wir uns an, wie das funktioniert:
Architektur-Flow
Wir gehen hier von einem sehr einfachen Static-Website Szenario aus, bei dem die Domain über Route53 registriert und die Website in einem S3-Bucket gehostet ist. Zusätzlich ist die Domain auch für AWS SES verifiziert.
Hier der Architektur-Flow für das Kontaktformular:
Die Informationen fließen in 3 einfachen Schritten:
- Das Kontakformular sammelt alle Informationen über den Benutzer und sendet diese an einen Endpunkt vom API Gateway (Zum Schutz vor Spam-Bots und anderen Angreifern wird AWS WAF eingesetzt)
- Das API Gateway leitet diese Informationen an eine Lambda Funktion weiter
- Die Lambda Funktion generiert automatisch eine E-Mail und sendet diese über den Amazon Simple Email Service an eine konfigurierte E-Mail-Adresse des Unternehmens
Aufsetzen des API Gateways
Zuerst setzen wir das API Gateway auf, um einen Http-Endpunkt für unser Kontaktformular zur Verfügung zu stellen. Dieser Endpunkt leitet die Informationen anschließend an die Lambda-Funktion weiter, die wir als Nächstes erstellen werden. Wir nutzen CloudFormation und AWS SAM, um unsere Infrastruktur wiederverwendbar darzustellen. Dazu legen wir zunächst eine Resource für die API an:
1Api:2Type: AWS::Serverless::Api3Properties:4Name: contact-form-api5StageName: production6DefinitionBody:7openapi: '3.0.1'8paths:9/contact:10post:11requestBody:12content:13application/json:14schema:15$ref: '#/components/schemas/contactform'16required: true17x-amazon-apigateway-integration:18type: 'aws_proxy'19uri: arn:aws:apigateway:eu-central-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-central-1:123456:function:contact-form:live/invocations20passthroughBehavior: 'when_no_match'21httpMethod: 'POST'22Models:23ContactForm:24type: object25required:26- mail27- name28properties:29mail:30type: string31name:32type: string33appointment:34type: string
Der POST-Endpunkt unserer API erwartet folgenden Request-Body:
1{2"mail": "someone@triplecore.io",3"name": "Someone",4"appointment": "ASAP"5}
Erstellen der Lambda Funktion
Der nächste Schritt besteht darin, eine Lambda-Funktion zu erstellen, die vom API-Gateway angesprochen wird und AWS SES nutzt, um eine E-Mail zu versenden. Die Lambda-Funktion sieht ungefähr so aus:
Wir stellen der Lambda-Funktion drei Environment-Variablen zur Verfügung(TEMPLATE, SENDER, RECIPIENT). Diese werden über das Cloudformation Template befüllt.1 const aws = require('aws-sdk');2 const ses = new aws.SES();34 exports.handler = async (event) => {5 const requestBody = JSON.parse(event.body);6 if (!requestBody.name || !requestBody.mail) {7 return {8 statusCode: 400,9 body: JSON.stringify({ error_message: 'Missing parameters' }),10 };11 }1213 const params = {14 Destination: {15 ToAddresses: [process.env.RECIPIENT],16 },17 Source: process.env.SENDER,18 Template: process.env.TEMPLATE,19 TemplateData: JSON.stringify(requestBody),20 };2122 return ses23 .sendTemplatedEmail(params)24 .promise()25 .then(() => {26 return { statusCode: 200, body: JSON.stringify({}) };27 })28 .catch((err) => {29 return {30 statusCode: 500,31 body: JSON.stringify({ error_message: err.message }),32 };33 });34 };
Als nächstes müssen wir unsere Lambda Funktion auch in unserem Cloudformation Template anlegen:
1ContactFormLambda:2Type: AWS::Serverless::Function3Properties:4Description: Lambda function to send email notifications for our contact form5CodeUri: contact-form6Handler: index.handler7FunctionName: contact-form8Runtime: 'nodejs12.x'9Role: !GetAtt ContactFormLambdaIamRole.Arn10Events:11ApiEvent:12Type: Api13Properties:14Path: /contact15Method: post16RestApiId: !Ref Api17RequestModel:18Model: ContactForm19Required: true20RequestParameters:21- method.request.querystring.error22Environment:23Variables:24RECIPIENT: info@example.com25SENDER: info@example.com26TEMPLATE: contact-form-template27AutoPublishAlias: live28DeploymentPreference:29Type: AllAtOnce30ContactFormLambdaIamRole:31Type: AWS::IAM::Role32Properties:33AssumeRolePolicyDocument:34Version: 2012-10-1735Statement:36- Effect: Allow37Principal:38Service:39- lambda.amazonaws.com40Action:41- sts:AssumeRole42RoleName: contact-form43Policies:44- PolicyName: contact-form45PolicyDocument:46Version: 2012-10-1747Statement:48- Effect: Allow49Action:50- ses:SendTemplatedEmail51- logs:*52Resource: '*'
Zusätzlich zur Lambda-Funktion legen wir auch noch eine IAM-Rolle an. Diese Rolle bekommt zwei Permissions, eine zum Senden der Email und die andere um Logs in Cloudwatch zu erzeugen.
Der Code für die Lambda-Funktion liegt in einem Unterordner unter contact-form
. Im Template referenzieren wir dann über CodeUri auf diesen Ordner.
Konfigurieren von AWS SES und anlegen des Email-Templates
Wie bereits im letzten Abschnitt erwähnt, müssen für SES die Sender und Empfänger Email-Adressen oder Domains verifiziert werden. Alternativ kann auch der Production-Mode über den Support angefragt werden. Dies sollte allerdings nur gemacht werden, wenn wirklich benötigt. Für unseren Fall reicht der Sandbox-Mode völlig aus. Weitere Infos dazu findet ihr in der offiziellen Dokumentation
Als nächsten Schritt wollen wir nun ein Template für unsere Email anlegen. Leider unterstützt Cloudformation momentan noch nicht das Anlegen von Template in eu-central-1
. Aus diesem Grund müssen wir das Template manuell anlegen.
Im einfachsten Fall sieht das Template folgendermaßen aus:
1Template:2HtmlPart: '<h1>Neue Kontaktanfrage</h1><p>Name: {{name}}</p><>Mail: {{mail}}</p>'3SubjectPart: 'Neue Kontaktanfrage'4TemplateName: 'website-api-contact-form'5TextPart: 'Neue Kontaktanfrage: Name: {{name}} Mail: {{mail}}'
Das Template kann anschließend über die AWS-CLI angelegt werden:
1aws ses create-template --region eu-central-1 --cli-input-yaml file://email-template.yml
Absichern des Endpunkts über AWS WAF (Web-Application-Firewall)
Niemand mag Captcha´s und oftmals erhöhen sie auch die Absprungrate deutlich. Und auch wenn Google mit dem Invisible ReCaptcha eine Verbesserung geschaffen hat, möchte man das seinen Nutzern (auch aus datenschutzrechlicher Sicht) nicht zwingend zumuten. Jedoch ist es eine noch viele schlechtere Idee, den Endpunkt einfach so und ungeschützt ins Internet zu stellen. Eine einfache Lösung, hier mehr Sicherheit zu schaffen, ohne die Nutzererfahrung zu verschlechtern ist AWS WAF.
Dies ist eine Firewall für Webanwendungen, die wir vor unser API-Gateway schalten können, um HTTP- und HTTPS-Anfragen zu überwachen. Hiermit haben wir die Möglichkeit eigene Regeln zu definieren oder bereits von AWS konfigurierte Regeln zu nutzen.
AWS bietet bereits eine Reihe von verwalteten Regelgruppen an, die Schutz vor häufigen Anwendungsschwachstellen oder anderem unerwünschten Datenverkehr bieten, ohne dass wir eigene Regeln schreiben müssen.
Für unseren Use-Case verwenden wir die folgenden 4 Managed Rules:
- Core Rule Set
Das Core Rule Set enthält Regeln, die allgemein für Webanwendungen gelten. Dies bietet Schutz vor Ausnutzung einer Vielzahl von Schwachstellen, die in OWASP-Publikationen erwähnt werden. Diese Regel sollte für jeden WAF Use-Case genutzt werden.
- Known Bad Inputs
Die Regelgruppe "Known Bad Inputs" enthält Regeln zum Blockieren von Request-Mustern, die bekanntermaßen ungültig sind. Dies kann dazu beitragen, das Risiko zu verringern, dass ein Angreifer eine gefährdete Anwendung entdeckt.
- IP Reputation List
Diese Gruppe enthält Regeln, die auf interner Threat Intelligence von Amazon basieren. Dies ist hilfreich, wenn man IP-Adressen blockieren möchten, die typischerweise mit Bots oder anderen Bedrohungen verbunden sind. Das Blockieren dieser IP-Adressen kann dazu beitragen, Bots zu minimieren.
- Anonymous IP List
Die Regelgruppe „Anonymous IP List“ enthält Regeln für das Blockieren von Anfragen über Services, die die Verschleierung der Viewer-Identität ermöglichen. Dazu gehören Anfragen über VPNs, Proxys, Tor-Knoten und Hosting-Anbieter (einschließlich AWS). Diese Regelgruppe ist nützlich, wenn Sie Betrachter herausfiltern möchten, die möglicherweise versuchen, ihre Identität vor Ihrer Anwendung zu verbergen. Das Blockieren der IP-Adressen dieser Services kann dazu beitragen, Bots und Möglichkeiten zur Umgehung geografischer Einschränkungen zu minimieren.
Zusätzlich zu den von AWS Managed Rules wollen wir eine zusätzliche ratenbasierte Regel selbst definieren. Diese Regel zählt alle Anfragen an unseren POST-Endpunkt und blockiert IP-Adressen, die mehr als 100mal innerhalb von 5 Minuten unseren Endpunkt aufzurufen. Für unseren Fall sollte das völlig ausreichen. Es ist jedoch auch möglich noch weitere Einschränkungen vorzunehmen, bspw. könnte man auch bestimmte Geographische Regionen auschließen.
Die Konfiguration für die WAF haben wir folgendermaßen in Cloudformation angelegt:
1WAFWebACL:2Type: AWS::WAFv2::WebACL3Properties:4Description: WebACL for website api5DefaultAction:6Allow: {}7Name: web-acl8Scope: REGIONAL9VisibilityConfig:10CloudWatchMetricsEnabled: true11MetricName: web-acl12SampledRequestsEnabled: true13Rules:14- Name: amazon-ip-reputation-list15OverrideAction:16None: {}17Priority: 018Statement:19ManagedRuleGroupStatement:20Name: AWSManagedRulesAmazonIpReputationList21VendorName: AWS22VisibilityConfig:23CloudWatchMetricsEnabled: true24MetricName: amazon-ip-reputation-list25SampledRequestsEnabled: true26- Name: common-rule-set27OverrideAction:28None: {}29Priority: 1030Statement:31ManagedRuleGroupStatement:32Name: AWSManagedRulesCommonRuleSet33VendorName: AWS34VisibilityConfig:35CloudWatchMetricsEnabled: true36MetricName: common-rule-set37SampledRequestsEnabled: true38- Name: known-bad-inputs-rule-set39OverrideAction:40None: {}41Priority: 2042Statement:43ManagedRuleGroupStatement:44Name: AWSManagedRulesKnownBadInputsRuleSet45VendorName: AWS46VisibilityConfig:47CloudWatchMetricsEnabled: true48MetricName: known-bad-inputs-rule-set49SampledRequestsEnabled: true50- Name: anonymous-ip-list51OverrideAction:52None: {}53Priority: 3054Statement:55ManagedRuleGroupStatement:56Name: AWSManagedRulesAnonymousIpList57VendorName: AWS58VisibilityConfig:59CloudWatchMetricsEnabled: true60MetricName: anonymous-ip-list61SampledRequestsEnabled: true62- Name: contact-form-rule63Action:64Block: {}65Priority: 4066Statement:67RateBasedStatement:68Limit: 10069AggregateKeyType: IP70ScopeDownStatement:71ByteMatchStatement:72FieldToMatch:73UriPath: {}74PositionalConstraint: EXACTLY75SearchString: '/contact'76TextTransformations:77- Type: NONE78Priority: 079VisibilityConfig:80CloudWatchMetricsEnabled: true81MetricName: contact-form-rule82SampledRequestsEnabled: true83APIWebACLAssociation:84DependsOn:85- WAFWebACL86- Api87Type: AWS::WAFv2::WebACLAssociation88Properties:89ResourceArn: arn:aws:apigateway:eu-central-1::/restapis/api-id/stages/production90WebACLArn: !GetAtt WAFWebACL.Arn
Fazit
Hier adressieren wir einen allgemeinen Anwendungsfall - ein einfaches Kontaktformular -, welches für jedes kleine Unternehmen wichtig ist, das seine Website auf Amazon S3 hostet. Dieser Beitrag soll dazu beitragen, Ihre statische Website dynamischer zu gestalten, ohne einen Server hochzufahren und gleichzeitig auch ein gewisses Maß an Sicherheit zu implementieren.