Manage WAF IP Access Rules with Cloudflare API

IP Access Rules adalah tool WAF dari Cloudflare yang digunakan untuk memblokir lalu lintas yang diduga berbahaya berdasarkan IP address, country, atau Autonomous System Number (ASN).

Jika Anda sudah menggunakan WAF custom rules, perlu diingat bahwa menambahkan IP, ASN, atau country pada IP Access Rules akan membypass custom rules, rate limiting rules, dan firewall rules (deeprecated).

Untuk mempermudah dalam mengelola WAF IP Access Rules menggunakan Cloudflare API, pada panduan ini akan menggunakan script yang ditulis menggunakan bahasa GO.

Setup Go

Download Go untuk Linux melalui go.dev/dl

wget https://go.dev/dl/go1.23.2.linux-amd64.tar.gz

Kemudian extract archive ke /usr/local.

tar -xaf go1.23.2.linux-amd64.tar.gz -C /usr/local/

Selanjutnya edit file .bash_profile lalu tambahkan baris berikut.

export PATH=$PATH:/usr/local/go/bin

Setelah itu, Anda dapat mengecek versi Go dengan perintah.

$ go version
go version go1.23.2 linux/amd64

Example Script

Untuk script dapat diambil dari GitHub atau bisa juga Anda copy dari sini.

Buat file cloudflare.go.

nano cloudflare.go

Lalu tambahkan kode berikut.

package main

import (
    "bytes"
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "text/tabwriter"
)

func main() {
    mode := flag.String("mode", "", "Mode for the firewall rule")
    ip := flag.String("ip", "", "IP address for the firewall rule")
    notes := flag.String("notes", "", "Notes for the firewall rule")
    del := flag.Bool("del", false, "Delete a firewall rule")
    flag.Parse()

    account := "[email protected]"
    key := "123"

    client := &http.Client{}

    if *mode == "" && *ip == "" && !*del {
        // Perform GET request to check existing rules
        url := "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules"
        method := "GET"

        req, err := http.NewRequest(method, url, nil)
        if err != nil {
            fmt.Println("Error creating GET request:", err)
            return
        }

        req.Header.Add("Content-Type", "application/json")
        req.Header.Add("X-Auth-Email", account)
        req.Header.Add("X-Auth-Key", key)

        res, err := client.Do(req)
        if err != nil {
            fmt.Println("Error executing GET request:", err)
            return
        }
        defer res.Body.Close()

        body, err := ioutil.ReadAll(res.Body)
        if err != nil {
            fmt.Println("Error reading GET response body:", err)
            return
        }

        var result map[string]interface{}
        if err := json.Unmarshal(body, &result); err != nil {
            fmt.Println("Error parsing GET response JSON:", err)
            return
        }

        if success, ok := result["success"].(bool); ok && !success {
            fmt.Println("API returned an error:", result["errors"])
            return
        }

        // Print the response in a formatted table
        writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)
        fmt.Fprintln(writer, "ID\tIP\tMode\tDate\tNotes")
        fmt.Fprintln(writer, "================================\t===============\t====\t====\t=====")
        for _, rule := range result["result"].([]interface{}) {
            ruleMap := rule.(map[string]interface{})
            id := ruleMap["id"]
            ip := ruleMap["configuration"].(map[string]interface{})["value"]
            mode := ruleMap["mode"]
            date := ruleMap["created_on"]
            notes := ruleMap["notes"]
            fmt.Fprintf(writer, "%v\t%v\t%v\t%v\t%v\n", id, ip, mode, date, notes)
        }
        writer.Flush()
    } else if *mode != "" && *ip != "" && !*del {
        // Perform GET request to check existing rules
        url := "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules"
        method := "GET"

        req, err := http.NewRequest(method, url, nil)
        if err != nil {
            fmt.Println("Error creating GET request:", err)
            return
        }

        req.Header.Add("Content-Type", "application/json")
        req.Header.Add("X-Auth-Email", account)
        req.Header.Add("X-Auth-Key", key)

        res, err := client.Do(req)
        if err != nil {
            fmt.Println("Error executing GET request:", err)
            return
        }
        defer res.Body.Close()

        body, err := ioutil.ReadAll(res.Body)
        if err != nil {
            fmt.Println("Error reading GET response body:", err)
            return
        }

        var result map[string]interface{}
        if err := json.Unmarshal(body, &result); err != nil {
            fmt.Println("Error parsing GET response JSON:", err)
            return
        }

        if success, ok := result["success"].(bool); ok && !success {
            fmt.Println("API returned an error:", result["errors"])
            return
        }

        ipExists := false
        if success, ok := result["success"].(bool); ok && success {
            if rules, ok := result["result"].([]interface{}); ok {
                for _, rule := range rules {
                    if ruleMap, ok := rule.(map[string]interface{}); ok {
                        if ruleMap["configuration"].(map[string]interface{})["value"] == *ip {
                            ipExists = true
                            break
                        }
                    }
                }
            }
        } else {
            fmt.Println("Failed to retrieve rules")
            return
        }

        if ipExists {
            fmt.Println("IP address already exists in the rules. Skipping POST request.")
            return
        }

        // Perform POST request
        url = "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules"
        method = "POST"

        data := fmt.Sprintf(`{
            "configuration": {
                "target": "ip",
                "value": "%s"
            },
            "mode": "%s",
            "notes": "%s"
        }`, *ip, *mode, *notes)

        req, err = http.NewRequest(method, url, bytes.NewBuffer([]byte(data)))
        if err != nil {
            fmt.Println("Error creating POST request:", err)
            return
        }

        req.Header.Add("Content-Type", "application/json")
        req.Header.Add("X-Auth-Email", account)
        req.Header.Add("X-Auth-Key", key)

        res, err = client.Do(req)
        if err != nil {
            fmt.Println("Error executing POST request:", err)
            return
        }
        defer res.Body.Close()

        fmt.Println("Response Status:", res.Status)
    } else if *del && *mode != "" && *ip != "" {
        // Perform GET request to find the rule ID
        url := "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules"
        method := "GET"

        req, err := http.NewRequest(method, url, nil)
        if err != nil {
            fmt.Println("Error creating GET request:", err)
            return
        }

        req.Header.Add("Content-Type", "application/json")
        req.Header.Add("X-Auth-Email", account)
        req.Header.Add("X-Auth-Key", key)

        res, err := client.Do(req)
        if err != nil {
            fmt.Println("Error executing GET request:", err)
            return
        }
        defer res.Body.Close()

        body, err := ioutil.ReadAll(res.Body)
        if err != nil {
            fmt.Println("Error reading GET response body:", err)
            return
        }

        var result map[string]interface{}
        if err := json.Unmarshal(body, &result); err != nil {
            fmt.Println("Error parsing GET response JSON:", err)
            return
        }

        if success, ok := result["success"].(bool); ok && !success {
            fmt.Println("API returned an error:", result["errors"])
            return
        }

        var ruleID string
        if success, ok := result["success"].(bool); ok && success {
            if rules, ok := result["result"].([]interface{}); ok {
                for _, rule := range rules {
                    if ruleMap, ok := rule.(map[string]interface{}); ok {
                        if ruleMap["configuration"].(map[string]interface{})["value"] == *ip && ruleMap["mode"] == *mode {
                            ruleID = ruleMap["id"].(string)
                            break
                        }
                    }
                }
            }
        } else {
            fmt.Println("Failed to retrieve rules")
            return
        }

        if ruleID == "" {
            fmt.Println("No matching rule found for deletion")
            return
        }

        // Perform DELETE request
        url = fmt.Sprintf("https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/%s", ruleID)
        method = "DELETE"

        req, err = http.NewRequest(method, url, nil)
        if err != nil {
            fmt.Println("Error creating DELETE request:", err)
            return
        }

        req.Header.Add("Content-Type", "application/json")
        req.Header.Add("X-Auth-Email", account)
        req.Header.Add("X-Auth-Key", key)

        res, err = client.Do(req)
        if err != nil {
            fmt.Println("Error executing DELETE request:", err)
            return
        }
        defer res.Body.Close()

        fmt.Println("Response Status:", res.Status)
    } else {
        fmt.Println("Invalid input. Use either GET request without parameters, POST request with --mode, --ip, and --notes parameters, or DELETE request with --del, --mode, and --ip parameters.")
        flag.Usage()
        os.Exit(1)
    }
}

Sesuaikan account menggunakan email yang terdaftar di Cloudflare dan key menggunakan Global API Key.

    account := "[email protected]"
    key := "123"

Untuk melihat list IP Access Rules.

go run cloudflare.go

Menambahkan IP ke daftar block.

go run cloudflare.go --mode block --ip 198.51.100.4 --notes "bruteforce"

Selain block, Anda juga dapat menggunakan mode challenge, whitelist, js_challenge, managed_challenge. Cek dokumentasi Create an IP Access rule.

Menghapus IP dari daftar block.

go run cloudflare.go --del --mode block --ip 198.51.100.4