Etiqueta: Vapor

  • Stopping DoS Attacks: Vapor + Redis

    Stopping DoS Attacks: Vapor + Redis

    Many iOS apps rely on custom APIs built with Vapor, and when those services become unavailable due to a DoS attack, the user blames the app, not the server. Showing iOS developers how to design safer backend interactions, implement rate-limiting with Redis, and understand threat models helps them build more resilient apps, anticipate real-world traffic spikes, and avoid outages that can ruin user experience.

    In this post, we’ll build a simple Vapor service encapsulated in a Docker container, and then integrate and configure Redis to help protect the service from DoS attacks.

    Hello Vapor Server

    Start by creating a new Vapor project using the following command:

    vapor new VaporDoS

    You will be asked about Fluent and Leaf, but we will not be working with databases (Fluent) or HTML templates (Leaf).

    Screenshot

    Navigate to the newly created folder. In this example, we’ll use Visual Studio Code as our source editor. Let’s begin by defining the dependencies and targets for our Vapor project:

    // swift-tools-version:5.9
    import PackageDescription
    
    let package = Package(
        name: "VaporDockerExample",
        platforms: [
            .macOS(.v13)
        ],
        dependencies: [
            .package(url: "https://github.com/vapor/vapor.git", from: "4.92.0")
        ],
        targets: [
            .target(
                name: "App",
                dependencies: [
                    .product(name: "Vapor", package: "vapor")
                ],
                path: "Sources/App"
            ),
            .executableTarget(
                name: "Run",
                dependencies: [
                    .target(name: "App")
                ],
                path: "Sources/Run"
            )
        ]
    )

    This Swift Package Manager manifest defines a Vapor-based server project called VaporDockerExample, specifying that it runs on macOS 13 or later and depends on the Vapor framework (version 4.92.0 or newer). It organizes the project into two targets: an App target that contains the application logic and imports the Vapor product, and a Run executable target that depends on the App target and serves as the entry point for launching the server. The file essentially tells SwiftPM how to build, structure, and run the Vapor application.

    Later on, we’ll continue by defining the configuration in configuration.swift:

    import Vapor
    
    public func configure(_ app: Application) throws {
        // Hostname: en Docker debe ser 0.0.0.0
        if let host = Environment.get("HOSTNAME") {
            app.http.server.configuration.hostname = host
        } else {
            app.http.server.configuration.hostname = "0.0.0.0"
        }
    
        // Puerto desde variable de entorno (útil para Docker / cloud)
        if let portEnv = Environment.get("PORT"), let port = Int(portEnv) {
            app.http.server.configuration.port = port
        } else {
            app.http.server.configuration.port = 8080
        }
    
        // Rutas
        try routes(app)
    }

    It configures a Vapor application by setting up its HTTP server hostname and port based on environment variables, which is particularly useful when running inside Docker or cloud environments. It first checks for a HOSTNAME variable to determine the server’s bind address, defaulting to 0.0.0.0 so the service is accessible from outside the container. It then reads the PORT environment variable to set the listening port, falling back to port 8080 if none is provided. Finally, it registers the application’s routes by calling routes(app), completing the server’s basic setup.

    Next, let’s define a new route for our hello endpoint:

    import Vapor
    
    func routes(_ app: Application) throws {
        app.get("hello") { req -> String in
            return "Hola desde Vapor dentro de Docker 👋"
        }
    
        app.get { req -> String in
            "Service OK"
        }
    }

    This code defines two HTTP routes for a Vapor application. The first route, accessible at /hello, responds to GET requests with the text “Hola desde Vapor dentro de Docker 👋”. The second route is the root endpoint /, which also responds to GET requests and returns “Service OK”. Together, these routes provide simple test endpoints to verify that the Vapor service is running correctly, especially when deployed inside Docker.

     
    import App
    import Vapor
    
    @main
    struct Run {
        static func main() throws {
            var env = try Environment.detect()
            try LoggingSystem.bootstrap(from: &env)
            let app = Application(env)
            defer { app.shutdown() }
            try configure(app)
            try app.run()
        }
    }

    This code defines the main entry point for a Vapor application, handling its full lifecycle from startup to shutdown. It begins by detecting the current runtime environment (such as development or production), initializes the logging system accordingly, and then creates an Application instance based on that environment. After ensuring the application will shut down cleanly when execution finishes, it calls configure(app) to set up server settings and routes, and finally starts the server with app.run(), making the Vapor service ready to accept incoming requests.

    Once we’ve finished implementing our Vapor code, the next step is to define a Dockerfile that will build and package our application into a lightweight image ready to run in any containerized environment.

    # Phase 1: build
    FROM swift:5.10-jammy AS build
    
    WORKDIR /app
    
    # Copy Package.swift and Source folder
    COPY Package.swift ./
    COPY Sources ./Sources
    
    # (Optional) resolve dependencies before, for cache
    RUN swift package resolve
    
    # Compile in release mode
    RUN swift build -c release --static-swift-stdlib
    
    # Phase 2: runtime
    FROM ubuntu:22.04 AS run
    
    # Minimal runtime dependencies for binary in Swift
    RUN apt-get update && \
        apt-get install -y \
        libbsd0 \
        libcurl4 \
        libxml2 \
        libz3-4 \
        && rm -rf /var/lib/apt/lists/*
    
    WORKDIR /run
    
    COPY --from=build /app/.build/release/Run ./
    
    EXPOSE 8080
    
    # In order vapor listens inside Docker
    ENV PORT=8080
    ENV HOSTNAME=0.0.0.0
    
    CMD ["./Run"]
    

    This Dockerfile builds and packages a Vapor application into a lightweight, production-ready container using a two-phase approach. The first phase uses the official Swift 5.10 image to compile the app in release mode with static Swift libraries, producing a small and efficient binary. It copies the project’s manifest and source code, resolves dependencies, and builds the executable. The second phase creates a minimal Ubuntu 22.04 runtime image, installs only the system libraries required by the Swift binary, and copies in the compiled Run executable from the build stage. It exposes port 8080, sets the necessary environment variables to ensure Vapor listens correctly inside Docker, and finally launches the server.

    This setup isn’t strictly necessary yet, but it prepares the project for a smooth integration of Redis in the next section.

    version: "3.8"
    
    services:
      vapor-app:
        build: .
        container_name: vapor-docker-example
        ports:
          - "8080:8080"
        environment:
          # Must match with what is being used in configure.swift
          PORT: "8080"
          HOSTNAME: "0.0.0.0"
        restart: unless-stopped

    This docker-compose.yml file defines a single service called vapor-app, which builds a Docker image from the current directory and runs the Vapor application inside a container named vapor-docker-example. It maps port 8080 on the host to port 8080 in the container so the Vapor server is accessible externally, and it sets the environment variables PORT and HOSTNAME to ensure the app listens correctly, matching the logic in configure.swift. The service is configured to automatically restart unless it’s explicitly stopped, making it more resilient in development or production environments.

    Build docker image and launch container by typing;

    docker composeup --build
    Screenshot

    Vapor server is ready, now lets call the endpoint for chacking that all is in place:

    Screenshot

    Configure Redis for avoiding DoS attacks

    A Denial-of-Service (DoS) attack occurs when a service is overwhelmed with excessive or malicious requests, exhausting its resources and preventing legitimate users from accessing it. Redis helps mitigate these attacks by serving as a fast, in-memory store that enables efficient rate limiting and request tracking; Vapor can use Redis to count requests per user or IP and reject or throttle those that exceed safe limits. By blocking abusive traffic early and cheaply, Redis prevents the application from being overloaded, keeping the service stable and responsive even under high or hostile load.

    First step is adding Redis library in Package.swift:

    // swift-tools-version:5.9
    import PackageDescription
    
    let package = Package(
       ...
        dependencies: [
           ...
            .package(url: "https://github.com/vapor/redis.git", from: "4.0.0")
        ],
        targets: [
            .target(
                name: "App",
                dependencies: [
                   ...
                    .product(name: "Redis", package: "redis")
                ],
                ...
        ]
    )
    

    Next step is configure Redis in configure.swift:

    import Vapor
    import Redis
    
    public func configure(_ app: Application) throws {
        ...
        // Config Redis
        let redisHostname = Environment.get("REDIS_HOST") ?? "redis"
        let redisPort = Environment.get("REDIS_PORT").flatMap(Int.init) ?? 6379
    
        app.redis.configuration = try .init(
            hostname: redisHostname,
            port: redisPort
        )
    
        // Middleware de rate limit
        app.middleware.use(RateLimitMiddleware())
    
        // Rutas
        try routes(app)
    }
    

    This code configures a Vapor application to connect to a Redis instance by reading its host and port from environment variables, which is useful when running in Docker or cloud environments. It retrieves REDIS_HOST and REDIS_PORT, falling back to "redis" and port 6379 if they aren’t provided, ensuring sensible defaults when using a Redis container. It then applies these values to app.redis.configuration, enabling the application to communicate with Redis for features such as caching, rate limiting, or request tracking.

    For easy testing, we’ll define a simple DoS protection rule stating that the /hello endpoint cannot be called more than twice every 30 seconds. This rule is implemented in RateLimitMiddleware.swift.

    import Vapor
    import Redis
    
    struct RateLimitMiddleware: AsyncMiddleware {
    
        // Maximum 2 request every 30 secs
        private let maxRequests = 2
        private let windowSeconds = 30
    
        func respond(
            to request: Request,
            chainingTo next: AsyncResponder
        ) async throws -> Response {
    
            // Only apply to /hello service
            guard request.url.path == "/hello" else {
                return try await next.respond(to: request)
            }
    
            let ip = request.remoteAddress?.ipAddress ?? "unknown"
            let key = "rate:\(ip)"
    
            // INCR key
            let incrResponse = try await request.redis.send(
                command: "INCR",
                with: [RESPValue(from: key)]
            )
    
            let newCount = incrResponse.int ?? 0
    
            // When is first time, we set window expiration
            if newCount == 1 {
                _ = try await request.redis.send(
                    command: "EXPIRE",
                    with: [
                        RESPValue(from: key),
                        RESPValue(from: windowSeconds)
                    ]
                )
            }
    
            // Limit overpassed
            if newCount > maxRequests {
                throw Abort(
                    .tooManyRequests,
                    reason: "You exeded the limit of 2 request every 30 secs on /hello endpoint."
                )
            }
    
            return try await next.respond(to: request)
        }
    }
    

    It defines an asynchronous rate-limiting middleware for Vapor that restricts access to the /hello endpoint by tracking requests in Redis. It identifies the client by IP address, increments a Redis counter (INCR) associated with that IP, and, on the first request within the time window, sets an expiration (EXPIRE) so the counter resets after 30 seconds. If the number of requests exceeds 2 within 30 seconds, the middleware throws a 429 Too Many Requests error with a descriptive message; otherwise, it allows the request to continue through the normal processing chain. This mechanism helps prevent abuse or DoS-like behavior on that specific route.

    Last but not least, we need to update our Docker configuration. The first step is to add the Redis environment variables to the Dockerfile.

    ...
    # In order vapor listens inside Docker
    ENV PORT=8080
    ENV HOSTNAME=0.0.0.0
    ENV REDIS_HOST=redis
    ENV REDIS_PORT=6379
    
    CMD ["./Run"]
    

    And finally update docker-compose.yml for adding Redis service and connect vapor-app with Redis.

    version: "3.8"
    
    services:
      vapor-app:
        build: .
        ports:
          - "8080:8080"
        environment:
          PORT: "8080"
          HOSTNAME: "0.0.0.0"
          REDIS_HOST: "redis"
          REDIS_PORT: "6379"
        depends_on:
          - redis
        restart: unless-stopped
    
      redis:
        image: redis:7-alpine
        container_name: redis-vapor
        ports:
          - "6379:6379"
        restart: unless-stopped

    Rebuild the image and launch the container:

    Screenshot

    Call /hello endpoint 3 times:

    Screenshot

    The rule that we have set is not very realistic, but is a clear example

    Conclusions

    Once you start implementing backend services, protecting your code against potential attacks becomes essential. The goal of this post was to show you how to build a simple defense mechanism against DoS attacks when working with Vapor, helping you keep your services stable, secure, and resilient under unexpected or malicious traffic.

    You can find source code used for writing this post in following repository

    References

  • Customizing Vapor Server Configurations

    Customizing Vapor Server Configurations

    Customizing environment variables and secrets for a Dockerized Vapor server bridges mobile app development with backend security and DevOps best practices. iOS developers working with Swift-based Vapor servers need to securely manage API keys, database credentials, and other sensitive configurations, especially in containerized environments. This post explores how to set up, inject, and manage these secrets effectively within Docker, helping developers build more secure and scalable backend solutions while enhancing their understanding of environment-based configuration management.

    In this post, we will configure a simple Vapor server and demonstrate how to set up custom environment variables and provide secrets to the app.

    Hello Vapor Server

    The first step is to create a Vapor project by entering the following command:

    vapor new HelloServer

    You will be asked about Fluent and Leaf, but we will not be working with databases (Fluent) or HTML templates (Leaf).

    Screenshot

    Navigate to the created folder. In our case, we will use Xcode as the source code editor.

    Open routes.swift file:

    import Vapor
    
    func routes(_ app: Application) throws {
        app.get("hello") { req -> String in
            let name = Environment.get("CUSTOM_VARIABLE") ?? "World"
            return "Hello, \(name)!"
        }
    }

    The given Swift code defines a simple web route using the Vapor framework. It sets up a GET endpoint at «/hello», which returns a greeting message. The message includes a name retrieved from an environment variable called "CUSTOM_VARIABLE"; if this variable is not set, it defaults to "World". So, when a user accesses GET /hello, the server responds with "Hello, [name]!", where [name] is either the value of "CUSTOM_VARIABLE" or "World" if the variable is absent

    Custom Configuration variables

    During project creation one of the files generated was a Dockerfile, this file without any update will be used for building a Docker image:

    docker build -t hello-vapor .

    Finally we will run the container:

    docker run -e CUSTOM_VARIABLE="Custom variable Value" -p 8080:8080 hello-vapor

    The command docker run -e CUSTOM_VARIABLE="Custom variable Value" -p 8080:8080 hello-vapor runs a Docker container from the hello-vapor image. It sets an environment variable CUSTOM_VARIABLE with the value "Custom variable Value" inside the container using the -e flag. The -p 8080:8080 flag maps port 8080 of the container to port 8080 on the host machine, allowing external access to any service running on that port inside the container.

    The Vapor server is up and waiting for ‘hello’ GET requests. Open an new terminal session window and type following command.

    curl http://localhost:8080/hello

    When we get back to server terminal window:

    The endpoint implementation accesses the CUSTOM_VARIABLE environment variable and prints its contents.

    Secrets

    When we talk about transferring sensitive information such as keys, passwords, and API tokens, there is no perfect solution, as even the most secure methods come with potential vulnerabilities. Here’s an overview of common approaches and their security concerns:

    1. Storing secrets in a .env file: This approach keeps secrets separate from the code but still requires careful management of the .env file to prevent unauthorized access.

    2. Using Docker Secrets with Docker Compose: Docker Secrets allow you to store sensitive data in encrypted files, which can be mounted into containers. However, careful access control is necessary to prevent unauthorized retrieval.

    3. Implementing a sidecar container for secret management: A separate container can handle secret retrieval and securely pass them to the main application container. Access control rules can be applied to limit access to a specific set of users.

    4. Employing external secret management tools: Solutions like HashiCorp Vault or cloud-based key management services provide robust secret handling and enhanced security features, but they may introduce complexity in management.

    In this guide, we will store the secrets in a .env file. The first step is to create a text file containing the key-value pairs, naming it .env.

    echo "SECRET=Asg992fA83bs7d==" >> .env

    Keep the file in a safe place but never in a repository.

    Update endpoint code for also fetching SECRET:

    import Vapor
    
    func routes(_ app: Application) throws {
        app.get("hello") { req -> String in
            let name = Environment.get("CUSTOM_VARIABLE") ?? "World"
            let secret = Environment.get("SECRET") ?? "---"
            return "Hello, \(name)! secret is \(secret)"
        }
    }
    

    Build a the new docker image:

    docker build -t hello-vapor .

    For executing container, as input parameter we provide the .env file that conains the secret:

    docker run --env-file .env -p 8080:8080 hello-vapor

    Server iss ready again:

    Calling endpoint again:

     

    curl http://localhost:8080/hello

    Secreta is now fetched by the endpoint.

    Conclusions

    For entering environment variables there is an standarized way of doing but we can not say the same when it comes to talk about secrets. Depending on the degree of security that we want to achieve we will have to apply one method or other.

    You can find source code used for writing this post in following repository

    References

  • Force Update iOS Apps When Backend Require It

    Force Update iOS Apps When Backend Require It

    In the mobile native (iOS/Android) app production ecosystem, multiple frontend versions often coexist and interact with the same backend. Frontend updates are typically adopted gradually; while it’s possible to enforce an update, this approach is generally considered disruptive and is used only in exceptional circumstances.

    This post aims to demonstrate a method for controlling request responses based on the frontend version specified in the request. The backend implementation will use Vapor, and the frontend will be an iOS app. Links to the GitHub repositories hosting the source code are provided at the end of this post.

    Keep request under control

    Including the client’s frontend version in backend requests is crucial for several reasons:

    1. Version-Specific Responses: The backend can tailor its responses to ensure compatibility and optimal functionality for each frontend version.

    2. API Versioning: It helps the backend serve the appropriate API version, supporting backward compatibility while enabling updates and improvements.

    3. Feature Support: Frontend versions may differ in their feature sets. The backend can adjust responses to include or exclude functionality based on the client’s capabilities.

    4. Performance Optimization: Backend processing and payloads can be optimized for the specific requirements of each frontend version, improving system performance.

    5. Error Handling: Knowing the frontend version allows for more relevant error messages and effective resolution of version-specific issues.

    6. Security Enhancements: Version-specific security protocols or restrictions can be implemented, boosting system security.

    By including the frontend version in client requests, developers can build robust, efficient, and maintainable systems that adapt to evolving requirements while maintaining compatibility with legacy clients.

    Vapor backend

    Vapor is an open-source web framework written in Swift, designed for building server-side applications. It offers a powerful and asynchronous platform for developing web applications, APIs, and backend services, all using Swift as the server-side language.

    This post is not a «build your first server-side app» tutorial. However, don’t worry—at the end of the post, I’ll share the tutorials I followed to gain a deeper understanding of this technology.

    To get started, we’ll create a new Vapor project. For this project, we won’t be working with databases, so you can safely answer «No» to all related prompts during the setup process.

    We will create an endpoint specifically for checking the minimum required versions compatible with the backend and determining whether a forced update is necessary. The endpoint will use the GET method, and the path will be /minversion.

    struct MainController: RouteCollection {
        func boot(routes: any Vapor.RoutesBuilder) throws {
            let minversionRoutesGrouped = routes.grouped("minversion")
            minversionRoutesGrouped.get(use: minVersion)

    And the associated function to perform this will be as follows.

        @Sendable
        func minVersion(req: Request) async throws -> VersionResponse {
            
            let currentVersion = "2.0.0"
            let minimumVersion = "1.5.0"
            let forceUpdate = true // o false dependiendo de la lógica de negocio
    
            // Devuelve la respuesta como JSON
            return VersionResponse(
                currentVersion: currentVersion,
                minimumVersion: minimumVersion,
                forceUpdate: forceUpdate
            )
        }

    Structure: We need to include the following information:

    1. Minimal Version: The minimum version of the application that the backend can handle.
    2. Current Version: The current version supported by the backend.
    3. Force Update: Whether a forced update is required.

    Instructions:
    Run the project, and check the log console to confirm that the server is ready.

    Use the curl command to call the specified endpoint.

    The API returns a JSON object containing the minimum and current versions, as well as a force-update flag.

    To simplify the backend’s ability to check frontend versions, we will add an additional attribute to each endpoint. This attribute will provide information about the frontend version. To illustrate this approach, we will create a sample POST endpoint that includes this feature.

    struct MainController: RouteCollection {
        func boot(routes: any Vapor.RoutesBuilder) throws {
            let minversionRoutesGrouped = routes.grouped("minversion")
            minversionRoutesGrouped.get(use: minVersion)
            
            let sampleRoutesGrouped = routes.grouped("sample")
            sampleRoutesGrouped.post(use: sample)
        }
    And its functionality is encapsulated in the following endpoint.
        @Sendable
        func sample(req: Request) async throws -> SampleResponse {
            let payload = try req.content.decode(SampleRequestData.self)
            let isLatestVersion =  await payload.version == VersionResponse.current().currentVersion
            let isForceUpdate = await VersionResponse.current().forceUpdate
            guard  isLatestVersion ||
                   !isForceUpdate else {
                throw Abort(.upgradeRequired) // Force update flag set
            }
    
            guard await isVersion(payload.version, inRange: (VersionResponse.current().minimumVersion, VersionResponse.current().currentVersion)) else {
                throw Abort(.upgradeRequired) // Version out of valid range
            }
            
            return SampleResponse(data: "Some data...")
        }

    The first thing the function does is validate that the version adheres to the X.Y.Z syntax.

        struct SampleRequestData: Content {
            let version: String
            
            mutating func afterDecode() throws {
                guard isValidVersionString(version) else {
                    throw Abort(.badRequest, reason: "Wrong version format")
                }
            }
            
            private func isValidVersionString(_ version: String) -> Bool {
                let versionRegex = #"^\d+\.\d+\.\d+$"#
                let predicate = NSPredicate(format: "SELF MATCHES %@", versionRegex)
                return predicate.evaluate(with: version)
            }
        }

    Later on, the process involves validating the version of a client application against a server-defined versioning policy. If the version check is successful, a simple JSON response with sample data is returned.

    Returning to the command line, we execute the sample using valid version values:

    We received a valid sample endpoint response, along with current, minimum version and wether forced update is being required.

    However, when we set a version lower than the required minimum, we encountered an error requesting an upgrade.

    While the implementation is theoretically complete, handling version updates on the front end is not difficult, but any mistakes in production can have dramatic consequences. For this reason, it is mandatory to implement a comprehensive set of unit tests to cover the implementation and ensure that when versions are updated, consistency is maintained.

    From now on, every new endpoint implemented by the server must perform this frontend version check, along with other checks, before proceeding. Additionally, the code must be data race-safe.

    At the time of writing this post, I encountered several issues while compiling the required libraries for Vapor. As a result, I had to revert these settings to continue writing this post. Apologies for the back-and-forth.

    IOS frontend

    The iOS app frontend we are developing will primarily interact with a sample POST API. This API accepts JSON data, which includes the current frontend version.

    • If the frontend version is within the supported range, the backend responds with the expected output for the sample POST API, along with information about the versions supported by the backend.
    • If the frontend version falls below the minimum supported version and a forced update is required, the backend will return an «update required» error response.

    To ensure compliance with Swift 6, make sure that Strict Concurrency Checking is set to Complete.

    … and Swift language version to Swift 6.

    Before we start coding, let’s set the app version. The version can be defined in many places, which can be quite confusing. Our goal is to set it in a single, consistent location.

    This is the unique place where you need to set the version number. For the rest of the target, we will inherit that value. When we set the version in the target, a default value (1.0) is already set, and it is completely isolated from the project. We are going to override this by setting MARKETING_VERSION to $(MARKETING_VERSION), so the value will be taken from the project’s MARKETING_VERSION.

    Once set, you will see that the value is adopted. One ring to rule them all.

    The application is not very complicated, and if you’re looking for implementation details, you can find the GitHub repository at the end of the post. Essentially, what it does is perform a sample request as soon as the view is shown.

    Make sure the Vapor server is running before launching the app on a simulator (not a real device, as you’re targeting localhost). You should see something like this:

    Simulator Screenshot - iPhone 16 Pro Max - 2024-12-05 at 12.05.23

    The current app version is 1.7.0, while the minimum supported backend version is 1.5.0, and the backend is currently at version 2.0.0. No forced update is required. Therefore, the UI displays a message informing users that they are within the supported version range, but it also indicates that an update to the latest version is available.

    Once we configure the Vapor backend to enforce a forced update:

            let versionResponse = VersionResponse(currentVersion: "2.0.0",
                                                  minimumVersion: "1.5.0",
                                                  forceUpdate: true)
            

    Re-run vapor server:

    Screenshot

    Re-run the app:

    Simulator Screenshot - iPhone 16 Pro Max - 2024-12-05 at 12.16.03

    The front-end needs to be updated, and users are required to update the app. Please provide a link to the Apple Store page for downloading the update.

    Conclusions

    In this post, I have demonstrated a method for versioning API communication between the backend and frontend. I acknowledge that my explanation of the implementation is brief, but you can find the backend and frontend repositories linked here.

    References