Running untrusted code with Fly.io
Mix.install([
{:kino, "~> 0.12.0"},
{:req, "~> 0.4.0"}
])
Intro
> Kuniyoshi’s Skeleton Specter
Who am I
Hello, I’m https://lubien.dev
- Working at Fly.io for almost 3 years
- Doing a lot of fullstack Elixir work and sometimes platform work.
- I like to figure things out
- Follow me on https://x.com/joao_lubien to see random code stuff
Part 1: A bit of a backstory
https://github.com/lubien/fly-together
https://fly.io/blog/remote-ide-machines/
Understanding Fly.io
Kino.Mermaid.new("""
graph TD;
Users-->|has 1+| Organizations;
Organizations-->|has 0+| Apps;
Apps-->|has 0+| Machines;
""")
graph TD;
Users-->|has 1+| Organizations;
Organizations-->|has 0+| Apps;
Apps-->|has 0+| Machines;
Setup Organization and Token
import Kino.Shorts
Kino.Shorts
org_slug = read_text("Organization slug")
if org_slug == "" do
Kino.interrupt!(:error, "Don't forget your organization slug")
end
url = "https://fly.io/dashboard/#{org_slug}/tokens"
Kino.Markdown.new("""
### Token
[Get your token at #{url}](#{url})
""")
fly_api_token = read_password("Fly Token")
if fly_api_token == "" do
Kino.interrupt!(:error, "We can't do anything without a fly_api_token")
end
Application.put_env(:presentation, :fly_api_token, fly_api_token)
fly_api_token
|> String.slice(0..15)
|> IO.puts()
FlyV1 fm2_lJPECA
:ok
Quick Fly.io library code
defmodule Fly do
def list_apps(org_slug) do
Req.get(req_config(), url: "/v1/apps?org_slug=#{org_slug}")
end
def create_app(org_slug, app_name, network) do
Req.post(req_config(),
url: "/v1/apps",
json: %{
app_name: app_name,
org_slug: org_slug,
network: network
}
)
end
def create_machine(app_name, config) do
Req.post(req_config(), url: "/v1/apps/#{app_name}/machines", json: config)
end
def allocate_ip(app_name, type) do
query = """
mutation ($input: AllocateIPAddressInput!) {
allocateIpAddress(input: $input) {
ipAddress {
address
}
}
}
"""
Req.post(graphql_config(),
json: %{query: query, variables: %{input: %{appId: app_name, type: type}}}
)
end
defp req_config do
Req.new(base_url: "https://api.machines.dev", auth: {:bearer, api_token()})
end
defp graphql_config do
Req.new(base_url: "https://api.fly.io/graphql", auth: {:bearer, api_token()})
end
defp api_token do
Application.get_env(:presentation, :fly_api_token)
end
end
"Created Fly module"
"Created Fly module"
List all your apps
button = Kino.Control.button("List apps") |> Kino.render()
Kino.animate(button, nil, fn _event, _state ->
{:ok, %{body: %{"apps" => apps}}} = Fly.list_apps(org_slug)
dt = Kino.DataTable.new(apps, name: "Apps")
{:cont, dt, nil}
end)
Shipping a livebook
config_for_image = fn region, image_ref, port ->
%{
"config" => %{
"env" => %{
"ELIXIR_ERL_OPTIONS" => "-proto_dist inet6_tcp",
"LIVEBOOK_DATA_PATH" => "/data",
"LIVEBOOK_HOME" => "/data",
"LIVEBOOK_ROOT_PATH" => "/data",
"LIVEBOOK_IP" => "::",
"LIVEBOOK_PASSWORD" => "dummypass123",
"PORT" => "8080"
},
"init" => %{},
"guest" => %{
"cpu_kind" => "shared",
"cpus" => 1,
"memory_mb" => 1024
},
"image" => image_ref,
"metadata" => %{},
"services" => [
%{
"autostart" => true,
"autostop" => true,
"force_instance_key" => nil,
"internal_port" => port,
"min_machines_running" => 0,
"ports" => [
%{
"force_https" => true,
"handlers" => ["http"],
"port" => 80
},
%{
"handlers" => ["http", "tls"],
"port" => 443
}
],
"protocol" => "tcp"
}
]
},
"lease_ttl" => 0,
"region" => region
}
end
#Function<40.105768164/3 in :erl_eval.expr/6>
frame = Kino.Frame.new()
handle_form = fn
%{data: %{action: :check} = data} ->
tree = Kino.Tree.new(config_for_image.(data.region, data.image, data.internal_port))
Kino.Frame.render(frame, tree)
%{data: %{action: :ship} = data} ->
Kino.Frame.clear(frame)
config = config_for_image.(data.region, data.image, data.internal_port)
md = Kino.Markdown.new("### Using config")
Kino.Frame.append(frame, md)
tree = Kino.Tree.new(config)
Kino.Frame.append(frame, tree)
case Fly.create_app(org_slug, data.app_name, "") do
{:ok, %{status: 201, body: body}} ->
md = Kino.Markdown.new("### Created app")
Kino.Frame.append(frame, md)
tree = Kino.Tree.new(body)
Kino.Frame.append(frame, tree)
{_state, res} ->
dbg(res)
Kino.interrupt!(:error, "Failed to create app, see error above")
end
case Fly.allocate_ip(data.app_name, "v4") do
{:ok, %{status: 200, body: body}} ->
md = Kino.Markdown.new("### Allocated IPv4")
Kino.Frame.append(frame, md)
tree = Kino.Tree.new(body)
Kino.Frame.append(frame, tree)
{_state, res} ->
dbg(res)
Kino.interrupt!(:error, "Failed to allocate IPv4, see error above")
end
case Fly.create_machine(data.app_name, config) do
{:ok, %{status: 200, body: %{"id" => id} = body}} ->
app_url = "https://#{data.app_name}.fly.dev"
dashboard_url = "http://fly.io/apps/#{data.app_name}"
md =
Kino.Markdown.new("""
### Created machine!

Machine ID: #{id}
- [Go to #{app_url}](#{app_url}) to see your app live
- [Go to #{dashboard_url}](#{dashboard_url}) to see your app in fly.io
- [Go to #{dashboard_url}/monitoring](#{dashboard_url}/monitoring) to see your app live logs
""")
Kino.Frame.append(frame, md)
tree = Kino.Tree.new(body)
Kino.Frame.append(frame, tree)
{_state, res} ->
dbg(res)
Kino.interrupt!(:error, "Failed to create machine, see error above")
end
end
#Function<42.105768164/1 in :erl_eval.expr/6>
config_form =
Kino.Control.form(
[
app_name: Kino.Input.text("App name", default: "livebook-app-#{Enum.random(10000..99999)}"),
image: Kino.Input.text("OCI image ref", default: "ghcr.io/livebook-dev/livebook:0.11.3"),
internal_port: Kino.Input.number("Internal Port", default: 8080),
region:
Kino.Input.select("Region", [{"gru", "São Paulo (gru)"}, {"gig", "Rio de Janeiro (gig)"}]),
action: Kino.Input.select("Action", check: "Check config", ship: "Ship machine!")
],
submit: "Submit"
)
config_form
Kino.listen(config_form, handle_form)
frame
Part 2: Great! Now you’ve made security folks mad
> Hackers destroying your product
Run this on your Livebook:
{:ok, res} = :inet_res.nslookup('_apps.internal', :in, :txt)
{:ok,
{:dns_rec, {:dns_header, 1, true, :query, true, false, true, true, false, 0},
[{:dns_query, ~c"_apps.internal", :txt, :in, false}],
[
{:dns_rr, ~c"_apps.internal", :txt, :in, 0, 5,
[
~c"achemists-db,alchemists,bugex-silent-cherry-2971,demo-alchemist-1,demo-alchemist-2,demo-alchemist-3,demo-alchemist-4,demo-alchemist-5,demo-alchemist-6,fly-builder-fragrant-sun-9317,javascriptbr-telegram-bot,jobs-bot,livebook-app-12289,livebook-app-13605,l",
~c"ivebook-app-13981,livebook-app-13982,livebook-app-13983,livebook-app-13984,livebook-app-16757,livebook-app-17250,livebook-app-22979,livebook-app-31584,livebook-app-47315,livebook-app-50515,livebook-app-51130,livebook-app-53014,livebook-app-57816,livebook-",
~c"app-60036,livebook-app-72646,livebook-app-78305,livebook-app-96320,livebook-app-97674,onlyferas,onlyferas-db,onlyferas-redis,townhall-1,townhall-2,townhall-5,townhall-6,user-code-d9717100-bcc4-4677-a2f9-a1349f459170,your-first-liveview-1"
], :undefined, [], false}
], [], []}}
{:ok,
{:dns_rec, {:dns_header, 1, true, :query, true, false, true, true, false, 0},
[{:dns_query, ~c"_apps.internal", :txt, :in, false}],
[
{:dns_rr, ~c"_apps.internal", :txt, :in, 0, 5,
[~c"achemists-db,alchemists,bugex-silent-cherry-2971,demo-alchemist-1,demo-alchemist-2,demo-alchemist-3,demo-alchemist-4,demo-alchemist-5,demo-alchemist-6,fly-builder-fragrant-sun-9317,javascriptbr-telegram-bot,jobs-bot,livebook-app-12289,livebook-app-13605,l",
~c"ivebook-app-13981,livebook-app-13982,livebook-app-13983,livebook-app-13984,livebook-app-16757,livebook-app-17250,livebook-app-22979,livebook-app-31584,livebook-app-47315,livebook-app-50515,livebook-app-51130,livebook-app-53014,livebook-app-57816,livebook-",
~c"app-60036,livebook-app-72646,livebook-app-78305,livebook-app-96320,livebook-app-97674,onlyferas,onlyferas-db,onlyferas-redis,townhall-1,townhall-2,townhall-5,townhall-6,user-code-d9717100-bcc4-4677-a2f9-a1349f459170,your-first-liveview-1"],
:undefined, [], false}
], [], []}}
6PNs
https://fly.io/docs/networking/private-networking/
Making untrusted code safe
Create one network per app.
- Fly.create_app(org_slug, data.app_name, "") do
+ Fly.create_app(org_slug, data.app_name, "#{data.app_name}-network")
Part 3: Why is this even useful?
Kino.Layout.grid(
[
Kino.Markdown.new(""),
Kino.Markdown.new(""),
Kino.Markdown.new(""),
Kino.Markdown.new(""),
Kino.Markdown.new("")
],
columns: 3
)
Last demo